Compare commits

...

81 Commits

Author SHA1 Message Date
JonKazama-Hellion 0c26d1aa67 merge: v1.1.0 Theme Foundation
First major UI cycle after the standalone v1.0.0 cut. Theme engine
with five built-in themes (Hellion Arctic, Chat 2 Klassik, Event
Horizon, Moonlit Bloom, Mint Grove), customisable JSON themes,
modernised settings layout (card-grid overview + breadcrumb detail
view), opt-in per-theme chat-channel colours, and the plugin icon
swap to the Hellion Forge hammer.

Configuration v13 → v14: all users land on Hellion Arctic. Pick
Chat 2 Klassik in Settings → Themes for the upstream look.

See HellionChat/HellionChat.yaml changelog and docs/CHANGELOG.md for
the full release notes; docs/THEME-AUTHORING.md is the new guide for
writing custom themes.
2026-05-05 15:17:14 +02:00
JonKazama-Hellion 8b13ba1fdc release(v1.1.0): updated screenshots and forge announce note
Three new screenshots replace the v1.0.x set: chatWindow (in-game
sidebar with FreeCompany tab), settingsOverview (card grid with all
nine sections), themesPicker (built-in themes plus example custom).
ImageUrls in repo.json + yaml updated; old withSimpleTweaks.png
dropped.

.github/forge-posts/v1.1.0.md seeded for the eventual auto-announce
workflow (Discord changelog post on tag push). Format matches the
forge-announce spec — frontmatter (subtitle, versionsnatur) plus DE
bullet body.
2026-05-05 15:14:50 +02:00
JonKazama-Hellion 52da5d5e23 chore(release): bump to v1.1.0 — theme foundation 2026-05-05 15:04:52 +02:00
JonKazama-Hellion 916640fb60 feat(brand): swap plugin icon to hellion forge hammer
The chat plugin now ships under the Hellion Forge plugin-workshop
brand. Icon is the 512x512 hammer mark from the Forge logo set
(was 256x256 ChatTwo derivative).
2026-05-05 15:00:08 +02:00
JonKazama-Hellion feeb1df4eb docs(themes): theme authoring guide with hellion forge branding 2026-05-05 14:54:58 +02:00
JonKazama-Hellion f2086865ce feat(themes): opt-in chat color apply banner in themes tab
When a theme defines its own chat channel colours and the current
Configuration.ChatColours don't match, a dezent banner offers Apply /
Keep — opt-in, never auto-overwriting user picks. Switching themes
re-arms the banner so each theme can be evaluated separately.
2026-05-05 14:51:16 +02:00
JonKazama-Hellion 15a89dd6e7 feat(themes): chat channel color sets for four built-in themes
Hellion Arctic, Event Horizon, Moonlit Bloom and Mint Grove each
ship a distinct chat-channel palette tinted toward their brand
family while preserving the FFXIV channel identity (Say light, Yell
yellow, Shout orange, Tell pink-magenta, Party blue, FC cyan, NN
green). Chat 2 Klassik intentionally ships without — users picking
that theme keep their existing channel colours.
2026-05-05 14:48:34 +02:00
JonKazama-Hellion 53952717c0 feat(themes): optional chat channel colors in theme schema 2026-05-05 14:44:59 +02:00
JonKazama-Hellion fcbbd174b6 fix(themes): wrap theme cards in begin/end group so the grid wraps
Theme-card grid was stacking diagonally for the same reason the
settings overview did: SetCursorScreenPos plus SameLine in the
caller loop don't compose. Wrap each card in BeginGroup/EndGroup,
draw name and author via DrawList instead of cursor hops, and let
ImGui handle row wrapping naturally.
2026-05-05 14:31:35 +02:00
JonKazama-Hellion d41cea0031 fix(settings): card grid wraps correctly, detail view drops legacy tab list
SettingsOverview now wraps each card in BeginGroup/EndGroup so SameLine
in the loop can wrap rows. The card content is drawn directly into the
DrawList (icon, title, subtext) without cursor hopping that broke the
flow.

DrawDetail no longer renders the second-column tab list — the user has
already picked a section from the overview, the redundant column made
the detail view feel like the old vanilla settings layout. Section
content now uses the full width.
2026-05-05 14:28:24 +02:00
JonKazama-Hellion c943a2cff3 fix(themes): drop legacy StyleModel push from chat log and pop-out
The pre-engine StyleModel override in ChatLogWindow.PreDraw and
Popout.PreDraw was layering an extra Dalamud style on top of the
Hellion theme, locally tinting the chat window back to a non-Hellion
look while every other plugin window rendered correctly. Theme is
now the single source of truth — pick chat2-classic for the upstream
flavour.
2026-05-05 14:23:41 +02:00
JonKazama-Hellion abcd0847ef fix(settings): restore cursor after card draw to keep grid layout intact 2026-05-05 14:22:23 +02:00
JonKazama-Hellion 2f52cbb7d4 feat(themes): seed example custom theme on first start 2026-05-05 14:17:22 +02:00
JonKazama-Hellion 9103bbb892 feat(settings): breadcrumb header and esc to return to overview 2026-05-05 14:15:12 +02:00
JonKazama-Hellion 8f9c01d322 feat(themes): mini-mockup preview in theme cards 2026-05-05 14:13:19 +02:00
JonKazama-Hellion af4651b37e feat(themes): export active theme to json 2026-05-05 14:11:14 +02:00
JonKazama-Hellion 485dc4e1b4 i18n(themes): localize theme settings card grid (en/de) 2026-05-05 14:09:02 +02:00
JonKazama-Hellion c878d24d11 feat(themes): settings tab with built-in and custom theme grids 2026-05-05 14:05:59 +02:00
JonKazama-Hellion cb5c940a84 feat(settings): card-grid overview router 2026-05-05 14:02:13 +02:00
JonKazama-Hellion dd3a0ea069 feat(themes): wire theme engine into plugin draw pipeline + migrate v13→v14
HellionStyle.PushGlobal nimmt jetzt eine Theme-Instance + Window-Opacity
und liest alle Color- und Style-Slots aus dem aktiven Theme statt aus
einer fixen Konstanten-Tabelle. Plugin hält die ThemeRegistry und schaltet
beim Init auf das in Config.Theme gespeicherte Slug.

Configuration v13 → v14:
- Neue Felder Theme (slug), WindowOpacity, ReduceMotion, UseCompactDensity,
  ShowThemeQuickPicker
- HellionThemeEnabled und HellionThemeWindowOpacity sind ab v14 [Obsolete]
  und bleiben bis v1.2.0 als JSON-Safety-Net erhalten
- Migration setzt alle Bestandsuser auf hellion-arctic; chat2-classic
  bleibt im Themes-Tab als Upstream-Look wählbar
- WindowOpacity übernimmt den Wert von HellionThemeWindowOpacity, alte
  HellionThemeEnabled-Flag entfällt funktional (Theme-Engine ist immer aktiv)

Konsumenten der alten Felder (ChatLogWindow.BgAlpha, Popout.BgAlpha) lesen
jetzt das neue WindowOpacity. Die Settings-UI in Appearance.cs schreibt
übergangsweise weiter in die Obsolete-Felder; Phase J ersetzt diesen Block
durch den dedizierten Themes-Tab. CS0612/CS0618 sind dort gezielt mit
pragma gekapselt.
2026-05-05 13:51:31 +02:00
JonKazama-Hellion 4bf6c3ef1f feat(themes): custom theme loading with file-stamp cache 2026-05-05 13:44:15 +02:00
JonKazama-Hellion 2378ce6bf2 feat(themes): json loader with schema validation 2026-05-05 13:42:40 +02:00
JonKazama-Hellion b85db24601 test(themes): sanity tests for all built-in themes 2026-05-05 10:28:08 +02:00
JonKazama-Hellion cae7d76206 feat(themes): theme registry with built-in lookup and fallback 2026-05-05 10:27:50 +02:00
JonKazama-Hellion 4c6d52e652 feat(themes): mint-grove built-in theme 2026-05-05 10:25:22 +02:00
JonKazama-Hellion cbfdfe35be feat(themes): moonlit-bloom built-in theme 2026-05-05 10:25:00 +02:00
JonKazama-Hellion 537b96c79f feat(themes): event-horizon built-in theme 2026-05-05 10:24:39 +02:00
JonKazama-Hellion d3d28924e6 feat(themes): chat2-classic built-in theme 2026-05-05 10:24:17 +02:00
JonKazama-Hellion 48f1fb5ba1 feat(themes): hellion-arctic built-in theme 2026-05-05 10:23:51 +02:00
JonKazama-Hellion 0b13efd0b5 feat(util): add HexToRgba parser for theme JSON 2026-05-05 10:21:46 +02:00
JonKazama-Hellion 289fe2eb78 feat(themes): theme top-level record 2026-05-05 10:19:33 +02:00
JonKazama-Hellion fe9e66b0ff feat(themes): theme typography record 2026-05-05 10:19:18 +02:00
JonKazama-Hellion 990edd8300 feat(themes): theme layout record 2026-05-05 10:19:03 +02:00
JonKazama-Hellion db95ec7dff feat(themes): theme colors record 2026-05-05 10:18:49 +02:00
JonKazama-Hellion 7e036c1d00 chore(csproj): enable nullable reference types
Audit-Tooling hatte einen mehrstündigen Sweep mit 50–200 erwarteten
Warnings prognostiziert. Tatsächliches Resultat: eine Zeile. Genau
eine. Codebase war pro-File längst nullable-konform, wir hatten den
Project-Switch nur nie umgelegt. Reminder dass Audit-Output ein
Hinweis ist, kein Plan, und ein menschlicher Pass davor lohnt sich.
2026-05-05 08:48:04 +02:00
JonKazama-Hellion 1c511a147d fix(stringutil): use InvariantCulture for byte-size formatting
Locale-Bug: BytesToString rendert auf deutscher Locale "1,5GB" statt
"1.5GB". InvariantCulture pinnt den Dezimal-Separator. Plus
InternalsVisibleTo-Hook für ein lokales (gitignored) Test-Projekt.
2026-05-05 08:34:56 +02:00
JonKazama-Hellion f093d93761 perf(messagemanager): switch pending queue to linked list, quiet privacy log
PendingSync läuft jetzt als LinkedList (O(1) Last statt O(n) Linq-Last
im ContentIdResolverHook); Privacy-Filter-Drop-Log auf Verbose runter,
sodass der Default-xllog-Stream nicht mehr pro Nachricht spammt.
2026-05-05 08:25:13 +02:00
JonKazama-Hellion e7c8667497 fix(emotecache): cancel pending texture loads on plugin dispose
Plugin-scoped CancellationTokenSource fließt jetzt durch LoadAsync und
die Texture-Calls; Dispose cancelt in-flight downloads. Smoke (System-
Spam + Reload) sauber, weiter beobachten unter höherem Emote-Volumen.
2026-05-05 08:09:53 +02:00
JonKazama-Hellion 497197eb2c chore(deps): cap major-bump packages with closed version ranges
ImageSharp, MessagePack and Pidgin pinned to [x.y, next-major) so a
lock-file regeneration cannot drift across a major. Resolved versions
unchanged; lock-file diff is request-string only.
2026-05-05 07:54:33 +02:00
JonKazama-Hellion 08b2ffc600 ci(codeql): pin actions to commit SHAs
Replaces floating major-version tags with full commit SHAs (Tag-
Kommentar dahinter), so a tag-republish can't slip a different action
into the workflow.
2026-05-05 07:45:37 +02:00
JonKazama-Hellion 8db3eca46c chore(fontmanager): drop unused Lodestone font download
The FontManager constructor downloaded FFXIV_Lodestone_SSF.ttf from
img.finalfantasyxiv.com on first start (or read it from a local
cache) into a GameSymFont byte array. Both historical readers of
that field are gone:

- BuildFonts() used to feed the bytes into AddFontFromMemory; that
  path was replaced by the Dalamud-provided AddGameSymbol helper.
- The upstream webinterface server wrote the bytes through a
  BinaryWriter to serve them to the Svelte frontend; the entire
  webinterface was intentionally removed in HellionChat.

With no live consumer left, the field, the constructor block, the
HttpClient call and the disk cache are all dead code. Removing them:

- eliminates the synchronous HTTP request on the plugin-load thread
  (no more multi-second startup hang on slow networks)
- closes the implicit "no timeout, no size guard" exposure on that
  request
- removes one outbound network endpoint (Square Enix Lodestone CDN)
  from the privacy footprint

PRIVACY.md and THIRD_PARTY_NOTICES.md updated to reflect that
HellionChat now talks to BetterTTV only (opt-out via setting). Cached
TTF files left over from earlier versions stay in pluginConfigs/
HellionChat/ until a user removes them; they are simply no longer
read.

Build: 0 warnings, 0 errors. No behavioural change for users — symbol
glyphs (job icons, item glyphs, status effects) keep rendering through
Dalamud's built-in symbol font.
2026-05-05 07:37:35 +02:00
JonKazama-Hellion 4d54eabdac chore: code quality sweep 2026-05-04 / 2026-05-05
General code-quality and robustness pass across the plugin: thread-
safety on IPC state, resource-disposal cleanups, input validation,
defensive null-checks and a few small UX glitches. Compliance docs
(THIRD_PARTY_NOTICES, PRIVACY, COPYRIGHT) refreshed to v1.0.3.

Highlights
- ExtraChat IPC state synchronised across threads
- ChatLogWindow autocomplete no longer leaks the unmanaged
  ImGuiListClipper allocation
- ChatLogWindow + Popout style stack stays balanced when config
  toggles mid-frame
- Retention sweep and privacy cleanup wait for the actual filter
  pass instead of the fire-and-forget Task that started it
- Configuration.LatestVersion bumped to 13 to match the active
  migration path
- GameFunctions placeholder buffer guarded against oversized
  replacement names
- TellTarget.IsSet, ResolveTempInputChannel, InputPreview, IconUtil,
  Lender, Payloads, ExtraPayload all hardened against null / empty /
  EOF / cycle inputs
- FontManager Lodestone download stays in scope for a follow-up
  (timeout + lazy init pending)
- AutoTranslate replaced the msvcrt.dll memcmp P/Invoke with a
  managed Span comparison
- Privacy cleanup worker thread marked IsBackground = true
- Database cleanup now removes both legacy files in one click
- Tell-target name redacted in the verbose debug log

Compliance
- THIRD_PARTY_NOTICES: last-reviewed bumped to v1.0.3, Pidgin 3.5.1,
  SQLitePCLRaw.lib.e_sqlite3 3.50.3 listed as direct dependency with
  CVE-2025-6965 / CVE-2025-7709 rationale
- PRIVACY: last-reviewed bumped to v1.0.3, BetterTTV trigger wording
  clarified (list fetch at startup vs. on-demand image fetch)
- COPYRIGHT: upstream attribution range widened

Build: 0 warnings, 0 errors. No behavioural changes that would alter
existing user configuration or stored chat history.
2026-05-05 07:28:12 +02:00
JonKazama-Hellion 698eb01bbe chore(release): bump to v1.0.3
v1.0.2 tag was claimed before the DownloadLink fix shipped, so the
content moves to v1.0.3. No code changes — manifest, repo.json,
CHANGELOG and README version refs roll forward; DownloadLink* URLs
now point to v1.0.3/latest.zip.
2026-05-04 16:17:06 +02:00
JonKazama-Hellion a3fbaab173 fix(release): bump DownloadLink URLs to v1.0.2
Manifest-bump in 8e9332a missed the three DownloadLink* entries.
Plugin-Manager fetched v1.0.1 zip (AssemblyVersion 1.0.1.0) against
the new repo.json (AssemblyVersion 1.0.2.0), tripping the version-
match guard.
2026-05-04 16:14:14 +02:00
JonKazama-Hellion 57291e925d merge: v1.0.2 polish patch 2026-05-04 16:00:07 +02:00
JonKazama-Hellion 8e9332ac8c chore(release): prepare v1.0.2 — polish patch
Four small backlog items bundled:

- New: hide chat (and every other plugin window) while the New Game+
  menu is open. Settings -> Window -> Frame, default off. Skips the
  whole WindowSystem.Draw() pass while QuestRedo is visible, mirroring
  the existing HideInLoadingScreens pattern.
- New: tint the channel selector button in the active channel colour.
  Settings -> Appearance -> Colours, default on. Reuses the existing
  inputColour computation (incl. ExtraChat override) and adds an
  ImGuiCol.Button push around the selector. New ColourUtil helper
  AdjustBrightness derives hover/active variants.
- Fix: PayloadHandler.InlineIcon hardcoded all hover icons to 32x32.
  Replaced with float-based aspect-ratio-preserving shrink, single
  scale-factor, zero-size guard, named MaxInlineIconSize constant.
  Affects six call sites (status, item, achievement and other inline
  hover paths).
- Diagnostic: HideState transitions log on Verbose level for both
  ChatLogWindow and Popout.

Manifest bumped to 1.0.2 across csproj, yaml, repo.json. CHANGELOG
entry added, README version line updated. yaml + repo.json changelog
trimmed to the slim 4-version window (1.0.2, 1.0.1, 1.0.0, 0.6.1).
2026-05-04 15:57:52 +02:00
JonKazama-Hellion fcb72e2b78 chore(release): prepare v1.0.1 — window position recovery
Bumps version to 1.0.1.0 and aligns the user-facing changelog across
HellionChat.csproj, HellionChat.yaml, repo.json and docs/CHANGELOG.md.

Headline fix: off-screen window recovery (one-shot bounds check on
plugin load + manual reset button under Settings -> Window -> Frame).
Bundled housekeeping since v1.0.0: docs restructured into docs/, stale
ChatTwo/* paths cleaned up across configs, Pidgin 3.3.0 -> 3.5.1,
actions/setup-dotnet 4 -> 5, github/codeql-action 3 -> 4.

DLL build verified locally; release.yml workflow generates the release
body from HellionChat.yaml on tag push.
2026-05-04 12:15:26 +02:00
JonKazama-Hellion 7012e8c0d8 feat(window): recover off-screen position after display layout change
Persisted ImGui window position can end up off-screen when the user
disconnects a monitor or changes display resolution between sessions.
The chat log window then renders outside the visible viewport with no
drag handles available, and the only recovery path is editing the JSON
config by hand.

This commit adds two layers of safety:

- Automatic one-shot bounds check on the first draw after plugin load.
  If less than 100x40 pixels of the saved window position overlap the
  primary viewport, the window snaps to a safe default offset
  (top-left + 50px). Logged at INF level so users can verify the
  recovery happened.
- Manual "Reset Window Position" button in Settings -> Window -> Frame
  as a deliberate escape hatch when anything else slips past the
  automatic check (different DPI scaling, viewport edge cases).

Pop-outs are intentionally not part of this recovery path: they are
non-persistent (cleared on plugin reload) and therefore cannot survive
a session boundary in an off-screen state.

Tested on Linux/Wayland (KAZAMA, Plasma, 3-monitor setup): hard-cut
test with both auxiliary monitors physically disconnected between
sessions reproduces the off-screen window before the patch and
recovers cleanly with this fix in place.
2026-05-04 12:01:43 +02:00
JonKazama-Hellion 176474ec2a chore(deps): bump Pidgin from 3.3.0 to 3.5.1
Catches up the only direct NuGet dependency that drifted behind on
the v1.0.0 standalone cut. The bump includes:

- 3.4.0: AnyCharExcept performance optimisation for single-char inputs
- 3.5.0: incremental parsing API in Pidgin.Incremental, public Expected
  constructors, SequenceTokenParser performance improvement
- 3.5.1: CIString Unicode handling fix (relevant for non-ASCII
  channel/tab names)

No security advisory drove this; rolling forward to align v1.0.0 with
the current upstream of every direct dependency. dotnet restore +
Release build verified locally, packages.lock.json regenerated.
2026-05-04 09:39:15 +02:00
JonKazama-Hellion 9fc8749d15 fix(repo): update stale ChatTwo paths in repo configs
- .gitattributes: linguist-generated path was still pointing at the
  pre-v1.0.0 ChatTwo/Resources/ tree, which silently let the renamed
  HellionChat/Resources/Language.*.resx files leak into Linguist's
  language statistics
- bug_report.yml: drop the "or ChatTwo" filter hint; the plugin only
  emits HellionChat.* into /xllog since the v1.0.0 standalone cut
2026-05-04 09:32:36 +02:00
dependabot[bot] 09634b416d chore(actions): Bump github/codeql-action from 3 to 4 (#7)
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 09:21:00 +02:00
dependabot[bot] 393ef175bf chore(actions): Bump actions/setup-dotnet from 4 to 5 (#6)
Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 4 to 5.
- [Release notes](https://github.com/actions/setup-dotnet/releases)
- [Commits](https://github.com/actions/setup-dotnet/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-dotnet
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-04 09:20:31 +02:00
JonKazama-Hellion d63c710836 docs: restructure into docs/ folder, add roadmap and learning notes
- Move AI_DISCLOSURE, THIRD_PARTY_NOTICES, UPSTREAM_SYNC, ipc.md
  into docs/ (ipc.md renamed to IPC.md for consistency)
- Add docs/ROADMAP.md, docs/CHANGELOG.md, docs/CONTRIBUTORS.md,
  docs/LEARNING-JOURNEY.md
- Update README to reflect the v1.0.0 standalone state, drop the
  development section, refresh the architecture tree, add a
  release-cadence block linking to LEARNING-JOURNEY
- Fix stale ChatTwo/* source paths to HellionChat/* across docs
- Update cross-links in PRIVACY, CONTRIBUTING and .github/* so they
  point at the new docs/ paths

Pure documentation pass, no code changes.
2026-05-04 09:03:59 +02:00
JonKazama-Hellion fa9baa3929 docs(changelog): document v12 -> v13 tab reset and pre-v13 backup
Adds a "Default tab layout sharpened" block between the Safety and
Crash-class sections in both yaml and repo.json. Explains the new
five-tab structure, calls out that the reset is one-time, that all
non-tab settings are preserved, and that the live config is backed
up to pluginConfigs/HellionChat.json.pre-v13-backup before the wipe
so users can restore manually.

The actual code change shipped in the previous commit; this commit
is purely the user-facing communication so the in-game migration
notification has matching written context.
2026-05-03 22:51:06 +02:00
JonKazama-Hellion 4c18b9a62b feat(tabs): sharpen default tab layout, hard-reset on v12 -> v13
Aligns the first-run tab layout with the sharpened defaults that
external testers asked for. Three changes in one commit:

1. VanillaGeneral now contains only Say/Yell/Shout. The previous
   30-channel kitchen-sink (party, FC, every linkshell, all gameplay
   events) buried the actual immediate-surroundings conversation
   under loot rolls, crafting and PF pings.
2. HellionSystem absorbs the gameplay-event streams that used to live
   in General — NpcDialogue, LootNotice, LootRoll, Crafting, Gathering,
   PeriodicRecruitmentNotification — plus the announcement and battle-
   system noise (BattleSystem, FreeCompanyAnnouncement,
   PvpTeamAnnouncement) that previously had no fixed home.
3. The first-run / wipe default no longer adds HellionBeginner
   conditionally and no longer adds a static VanillaTellExclusive tab.
   Auto-Tell-Tabs spawns per-conversation tabs on demand, the static
   tell-bucket is redundant. NoviceNetwork users can still add the
   Beginner preset from Settings -> Tabs.

A new v12 -> v13 migration triggers a hard tab-wipe on existing
installs because per-channel mapping from the old General preset to
the new General/System split is ambiguous. The wipe scope is
narrow: only Config.Tabs is cleared, every other knob (Privacy,
Retention, Theme, etc.) keeps its current value. A pre-v13 backup
of the live config is written alongside it for manual restore.
Users see the existing SettingsRefactor migration notification.
2026-05-03 22:48:21 +02:00
JonKazama-Hellion 26c12c3410 docs(ipc): rewrite IPC guide in Hellion-style
Replaces the inherited Chat-2 IPC guide with a guide that follows the
Hellion README structure: top-level intro, a dedicated "Compatibility
with Chat 2" section that calls out exactly what the v1.0.0 rename did
(and did not) change, a channel-reference table, per-surface sections
with separate "Migration from Chat 2" diff blocks, and a closing
license/attribution paragraph that points back to NOTICE.md.

Content additions on top of the previous version:

- Explicit statement that the IPC surface is the Chat-2 surface re-
  published under the Hellion name. Tuple shapes, lifecycle and call
  semantics are unchanged; only the channel-string prefix differs
- Channel-reference table at the top so integrators can see the full
  surface at a glance instead of reading two long code samples
- Tuple-payload field table for the Typing State IPC with a short
  meaning per field
- Behavior notes for ChatInputStateChanged (fires once on subscribe,
  then only on real changes) so integrators don't need to read the
  source to learn the contract
- Explicit migration-diff blocks for both surfaces

Existing example code is preserved with the same identifiers; only
the host-class names are aligned to "HellionChat..." instead of
the old "ChatTwoIpc"/"TypingIntegration" placeholders.
2026-05-03 22:32:17 +02:00
JonKazama-Hellion 76a4de1192 docs(ipc): align IPC integration guide with HellionChat.* channel names
ipc.md guides third-party plugin authors who want to bind to our
context-menu IPC and our typing-state IPC. After the v1.0.0 channel
rename (ChatTwo.* → HellionChat.*) the example code in this file no
longer matched the channels the plugin actually exposes — third-party
authors who copy-pasted from the doc would see silent no-op subscriptions.

Updated:
- All 6 channel string literals in code samples (Register, Unregister,
  Invoke, Available, GetChatInputState, ChatInputStateChanged)
- Code-path references in the Typing State IPC explanation block
  (HellionChat.Code.ChatType, HellionChat/Configuration.cs etc.)
- Class/variable name in the example (ChatTwoIpc → HellionChatIpc)
- Prose references to the plugin name

Added a short migration note at the top so existing integrators see
the rename in one paragraph instead of having to diff the file.
2026-05-03 22:29:07 +02:00
JonKazama-Hellion 55aeaea5b9 merge: v1.0.0 Standalone Rebrand & DAU Crash-Schutz
Brings the feature/hellionchat-rebrand-v1.0.0 branch into main as the
first fully standalone Hellion Chat release.

Includes:
- Rebrand from ChatTwo.* fork identity to standalone HellionChat
  identity (namespace, repo folder, IPC channels, ImGui IDs, build
  pipeline)
- Runtime detector that refuses to load the plugin when upstream
  Chat 2 is also active, preventing the parallel-load FFXIV crash
- Three critical and twenty-one major pre-existing defects fixed
  in the same release (CodeRabbit-flagged): AABB overlap correctness,
  Equals/GetHashCode anti-pattern, IPC dispose mismatch, IDisposable
  contract on DebuggerWindow, ExtraChat / GameFunctions null-deref
  guards, AutoTranslate concurrent-access lock, Privacy retention
  bounded waits, EmoteCache and FontManager HttpClient leaks,
  SearchSelector ImGui ID collision, DbViewer count caching, plus
  Sheets/Tabs/Popout bounds checks and the IconUtil binary-search
  off-by-one
- SQLite native binary pinned to 3.50.3 (CVE-2025-6965, CVE-2025-7709)
- packages.lock.json now enforced via RestorePackagesWithLockFile
- Public-facing branding text aligned to standalone framing while
  EUPL-1.2 license attribution is preserved unchanged

Verified locally on KAZAMA via dotnet build and manual FFXIV
smoketests A/B/C plus the post-fix sweep test.
2026-05-03 22:19:43 +02:00
JonKazama-Hellion e6d25f3e38 docs(changelog): expand v1.0.0 entry with full fix sweep
The original v1.0.0 changelog only documented the rebrand. After the
CodeRabbit pass added 9 follow-up fix commits (3 critical bugs plus
21 major findings, grouped into safety / crash-class / correctness /
threading / resource / performance categories), the changelog needs
to reflect what users are actually receiving.

yaml + repo.json synchronized.
2026-05-03 22:19:05 +02:00
JonKazama-Hellion 740c7cf1bb perf(dbviewer): cache filteredHistory.Count once per export
The DB export loop called filteredHistory.Count twice per 5000-message
batch — once for the progress fraction, once for the status text.
filteredHistory is built lazily by Filter() and re-enumerates on every
.Count access, so on a 2M-message history each batch was paying for
two full passes through the IEnumerable. Materializing the count once
at the top reduces the export to a single O(N) traversal as intended.

The RunOnTick + delayTicks pacing is intentional (keeps SeString
encoding on the framework thread and rate-limits the export to avoid
laggy frames during long exports), so the rest of the loop stays put.
2026-05-03 22:13:53 +02:00
JonKazama-Hellion 71f0b63079 build: harden NuGet restore and ship SQLite >= 3.50.3
Two pre-existing build/security defects flagged by CodeRabbit:

- HellionChat.csproj sets RestorePackagesWithLockFile=true so dotnet
  restore honors the committed packages.lock.json. Floating version
  ranges in the lockfile previously could drift between machines or
  CI runs, producing builds with subtly different transitive
  dependencies
- HellionChat.csproj pins SQLitePCLRaw.lib.e_sqlite3 to 3.50.3 to
  override the older 2.1.11 native build that
  Microsoft.Data.Sqlite 10.0.7 transitively pulls in. Ships SQLite
  3.50.3 which contains the fixes for CVE-2025-6965 (memory
  corruption from aggregate-term overflow) and CVE-2025-7709. The
  managed Microsoft.Data.Sqlite wrapper stays on 10.0.7 — only the
  native binary is bumped, no API breakage. Verified via the NuGet
  spec: "the first three numbers in the version number of this
  package indicate the version of SQLite that was used to build it"
2026-05-03 22:13:10 +02:00
JonKazama-Hellion 8ee54bb8df fix(http): close socket leaks in EmoteCache and FontManager
- EmoteCache.cs replaces the per-call "new HttpClient()" with the
  existing static Client field. The static instance already exists
  for two other endpoints in the same file and reuses connection
  pooling; the third call site was a stray that leaked a socket
  on every emote download
- FontManager.cs wraps both the HttpClient and the HttpResponseMessage
  in using-blocks, replaces the .Result/AggregateException sandwich
  with GetAwaiter().GetResult() for clean exception propagation, and
  adds EnsureSuccessStatusCode so failed downloads don't silently
  produce a zero-byte font file. Full async refactor of the FontManager
  constructor is tracked separately
2026-05-03 22:08:48 +02:00
JonKazama-Hellion e3ce41306e fix(threading): protect AutoTranslate cache and bound framework waits
- Util/AutoTranslate.cs introduces a single EntriesLock object and
  serializes every read and write of the static Entries dictionary
  and ValidEntries hash set behind it. PreloadCache spawns a worker
  thread that fills both while the main thread reads them via the
  Matching / ReplaceWithPayload / StartsWithCommand entry points;
  without the lock the underlying collection access was undefined.
  AllEntries() splits into a thin lock wrapper plus a private
  BuildEntriesLocked() helper that runs under the lock
- Ui/SettingsTabs/Privacy.cs bounds the .Wait() on the framework
  refresh after a manual retention sweep and after the privacy
  cleanup. A hung framework tick previously could deadlock the
  background worker thread. Five-second timeout, log on miss
2026-05-03 22:08:02 +02:00
JonKazama-Hellion af7c757e63 fix(ui): ImGui ID collisions and missing popup-open trigger
- Util/SearchSelector.cs ImRaii.PushId(id) collapsed every row in the
  filtered list to the same ImGui ID, leaving the ID stack ambiguous
  for click resolution. Mix the row index into the pushed id so every
  Selectable has a distinct ImGui identifier
- Ui/SettingsTabs/Chat.cs blocked-emote add-button never opened the
  selector popup because SearchSelector.SelectorPopup is wrapped in
  ImRaii.ContextPopupItem (right-click semantics). Detect the
  IsItemClicked() event after the button and call ImGui.OpenPopup
  explicitly so left-click opens the picker too
2026-05-03 22:05:57 +02:00
JonKazama-Hellion a10c115b9b fix: IDisposable contract + zh-Hans webinterface translation
- Ui/Debugger.cs DebuggerWindow now declares IDisposable so the
  existing Dispose() method participates in disposal patterns
  (using-blocks, container cleanup). Previously the method existed
  but the type didn't advertise it, so callers had no way to invoke
  it correctly and the command-handler subscription leaked
- Resources/Language.zh-Hans.resx Webinterface_Start_Success
  contained "网页界面已停止。" (web interface stopped) for what is
  semantically the start-success message; corrected to
  "网页界面已启动。" (web interface started). String is unused in
  the Hellion fork (webinterface removed) but remains in the
  resource bundle for upstream compatibility
2026-05-03 22:05:20 +02:00
JonKazama-Hellion 6d49dbad3e fix(ui): bounds-guard out-of-range list access in pop-out and tabs UI
Two pre-existing upstream defects fixed in v1.0.0:

- Ui/Popout.cs PopOutDocked[Idx] now bounds-checks Idx against
  ChatLogWindow.PopOutDocked.Count before reading or writing. A
  popout instance can outlive a list resize when AddPopOutsToDraw()
  rebuilds the docked-state list while a draw frame is in flight,
  which previously produced an out-of-range crash on tab drop
- Ui/SettingsTabs/Tabs.cs guards against an empty worlds list before
  indexing worlds[selectedWorld]. Empty lists can occur briefly when
  switching characters or before the datacenter sheet finishes
  loading — the previous code would crash with an
  ArgumentOutOfRangeException
2026-05-03 22:04:45 +02:00
JonKazama-Hellion a651b3b9ad fix: correctness bugs flagged by CodeRabbit
Four pre-existing upstream defects fixed in v1.0.0:

- Util/GlobalParametersCache.cs GetValue captures Cache into a local
  before the bounds check, so the check and the indexed read operate
  on the same array reference even when Refresh reassigns Cache from
  the main thread between the two operations
- Util/IconUtil.cs binary search bounds: hi initialized to
  entries.Length-1 (was Length), and reset on redirect-restart;
  added entries.Length==0 short-circuit to prevent indexing into
  empty arrays
- Sheets.cs WorldsOnDatacenter compared Region.RowId, which groups
  by region instead of datacenter — now compares DataCenter.RowId
  directly so the result actually reflects same-DC worlds
- Message.cs back-reference loop iterates the processed Sender/Content
  properties rather than the raw constructor parameters, so chunks
  added or replaced by CheckMessageContent also get Message set
2026-05-03 22:03:47 +02:00
JonKazama-Hellion 3f2e56be67 fix: tighten resource-leak and null-deref hot spots
Three pre-existing upstream defects flagged by CodeRabbit, fixed in the
v1.0.0 standalone cut where we own the codebase:

- Ipc/ExtraChat.cs Dispose now unsubscribes all three IPC subscriptions
  (OverrideChannelGate, ChannelCommandColoursGate, ChannelNamesGate)
  instead of only the first; previously the latter two leaked their
  subscriptions on every plugin reload
- GameFunctions/Types/TellTarget.cs FromTarget guards against a zero
  IPlayerCharacter.Address before dereferencing the unsafe Character*
  cast; previously a missing/destroyed target object would crash the
  game on /tell construction
- GameFunctions/GameFunctions.cs ResolveTextCommandPlaceholderDetour
  null-checks the Hook reference before calling .Original instead of
  using the null-forgiving operator; defensive guard for teardown races
2026-05-03 22:02:46 +02:00
JonKazama-Hellion feb6e262e4 fix(ipc): match Unregister call to Register call type in Dispose
UnregisterGate is registered via RegisterAction(Unregister) on
construction (Unregister returns void), but Dispose was calling
UnregisterFunc() instead of UnregisterAction(). The mismatched
unregister call leaks the action subscription on plugin reload —
subsequent Dispose/Init cycles would accumulate orphan handlers
in the Dalamud IPC layer.

Pre-existing upstream issue (CodeRabbit critical finding); fixed in
v1.0.0 standalone cut where we own the codebase.
2026-05-03 21:57:35 +02:00
JonKazama-Hellion 1d557f1b0e fix(code): replace GetHashCode comparison in ChatCode.Equals with field equality
Equals(object?) was delegating to GetHashCode() comparison, which is
the textbook hash-collision anti-pattern: two distinct ChatCode values
could in principle share a hash and be wrongly reported as equal. The
current GetHashCode implementation packs Type/Source/Target into 24
bits and happens to be collision-free, but the contract is fragile —
any future change to GetHashCode silently breaks Equals.

Replaced with direct field-by-field comparison of Type, Source, Target.
GetHashCode is left unchanged so dictionary/HashSet behavior stays
identical.

Pre-existing upstream issue (CodeRabbit critical finding); fixed in
v1.0.0 standalone cut where we own the codebase.
2026-05-03 21:57:17 +02:00
JonKazama-Hellion fea4965889 fix(util): correct AABB overlap test in MathUtil.HasOverlap
The previous implementation nested a ValueInRange helper that used
strict inequalities at both ends. That dropped identical rectangles
and shared-edge cases as false negatives — two rectangles with
a.X == b.X would miss the overlap on the X axis even when they
clearly overlapped.

Replaced with the standard AABB test: rectangles overlap iff they
overlap on both axes (a.Min < b.Max && a.Max > b.Min per axis).

Pre-existing upstream issue (CodeRabbit critical finding); fixed in
v1.0.0 standalone cut where we own the codebase.
2026-05-03 21:56:39 +02:00
JonKazama-Hellion 91663832f0 release: sync repo.json with v1.0.0 manifest
AssemblyVersion and TestingAssemblyVersion bumped to 1.0.0.0,
DownloadLinks (Install/Update/Testing) all point to
releases/download/v1.0.0/latest.zip, Changelog block mirrors
HellionChat.yaml.
2026-05-03 21:40:19 +02:00
JonKazama-Hellion 9cf1b19801 release: bump version to 1.0.0
First standalone major release. csproj version and yaml changelog
synchronized; repo.json sync follows in next commit.
2026-05-03 21:31:06 +02:00
JonKazama-Hellion 1f7f0945c5 build: rename repository folder ChatTwo to HellionChat
Repository folder, csproj, solution and all CI/build paths now use
the consolidated HellionChat name.

- ChatTwo/ → HellionChat/ (git mv preserves history with --follow)
- ChatTwo.csproj → HellionChat.csproj
- ChatTwo.sln → HellionChat.sln; obsolete Tests project entry removed
  (private/untracked sandbox)
- AssemblyInfo.cs InternalsVisibleTo for ChatTwo.Tests removed
  (file emptied; can be repopulated when actual tests land)
- repo.json and yaml image URLs updated (ChatTwo/images/ → HellionChat/images/)
- .github/workflows/{build,codeql,release}.yml csproj paths
- .github/dependabot.yml directory path

Functional behavior unchanged.
2026-05-03 21:30:07 +02:00
JonKazama-Hellion cd6afb32cb build: align RootNamespace with HellionChat post-rename
Updates the project root namespace and replaces the historical comment
about cherry-pick compatibility — that compatibility was the rationale
for keeping ChatTwo.* in source while AssemblyName was already
HellionChat. With v1.0.0 the standalone cut is complete and both
identifiers match.

Note: this commit also fixes the runtime MissingManifestResourceException
that the previous Task 6 commit caused — the embedded resource prefix
is derived from RootNamespace at build time, so Designer.cs string
arguments and the actual resource names only line up once both are set
to HellionChat.
2026-05-03 21:27:50 +02:00
JonKazama-Hellion 7d5496e959 refactor(namespace): rename ChatTwo.* to HellionChat.* across all source files
81 namespace declarations and 100 using directives converted via sed,
plus two FQN-aliases (ChatTwoPartyFinderPayload in PayloadHandler.cs and
ModifierFlag in KeybindManager.cs) updated. Critical: Language.Designer.cs
and HellionStrings.Designer.cs ResourceManager string arguments updated
synchronously — these are runtime reflection lookups not caught by the
C# compiler.

Two intentional ChatTwo references remain: the legacy migration path
'ChatTwo.json' in Plugin.cs (still points to upstream Chat 2's config
file by design) and the InternalsVisibleTo declaration in
AssemblyInfo.cs (handled in the upcoming repo-folder rename task).

The local alias names 'ChatTwoPartyFinderPayload' and 'ChatTwoConflictDetector'
are preserved as local symbols; only their target namespaces and references
changed.
2026-05-03 21:23:28 +02:00
JonKazama-Hellion ed426556e1 docs(branding): hellionize public-facing descriptions
README, repo.json and HellionChat.yaml describe the plugin as a
chat-replacement based on Chat 2 rather than as a Chat-2 fork. License
attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md, README
credits section) is left untouched per EUPL-1.2 obligations.

Architecture section in README updated to reflect that the
HellionChat namespace is now consolidated (post-v1.0.0).
2026-05-03 21:20:50 +02:00
JonKazama-Hellion 96c445356b refactor(strings): hellionize functional comparison wording
Three user-facing strings reworded from upstream-comparison framing to
neutral phrasing. License attribution and personal-story strings remain
unchanged (EUPL-1.2 attribution, author's voice).

- Privacy filter description (EN/DE)
- Full-history mode tooltip (EN/DE)
- Colour preset 'ChatTwo Default'/'ChatTwo Standard' becomes
  'Klassik (Chat 2 Default)' in both languages
2026-05-03 21:15:30 +02:00
JonKazama-Hellion 1c2d361b77 feat(safety): refuse to load when Chat 2 is also active
When the user has both Hellion Chat and upstream Chat 2 installed and
loaded, the parallel chat-window replacement and Hook collisions can
crash FFXIV at frame boundaries. The new detector inspects
IDalamudPluginInterface.InstalledPlugins and throws an
InvalidOperationException with a localized message if Chat 2 is loaded,
which Dalamud surfaces cleanly instead of letting the load proceed
into a runtime crash.

Bilingual messages (EN/DE) follow the existing HellionStrings pattern.
2026-05-03 21:08:55 +02:00
JonKazama-Hellion 581aae1735 refactor(ui): rename ImGui popup ID to hellionchat- prefix
Prevents ImGui ID-stack collision when both Hellion Chat and upstream
Chat 2 try to render their context popups in the same frame.
2026-05-03 21:07:21 +02:00
JonKazama-Hellion 70109e1896 refactor(ipc): rename IPC channels from ChatTwo.* to HellionChat.*
All 6 inter-plugin communication channels are renamed for the v1.0.0
standalone cut. Prevents Dalamud IPC registration conflicts when a user
has both Hellion Chat and upstream Chat 2 installed.

- IpcManager: Register, Available, Unregister, Invoke
- TypingIpc:  GetChatInputState, ChatInputStateChanged

Breaking change for third-party plugins that bound to ChatTwo.* — none
known at the time of this commit.
2026-05-03 21:07:05 +02:00
160 changed files with 4356 additions and 1258 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
# Generated files
ChatTwo/Resources/Language.*.resx linguist-generated=true
HellionChat/Resources/Language.*.resx linguist-generated=true
+1 -1
View File
@@ -59,7 +59,7 @@ body:
id: log
attributes:
label: Relevant /xllog excerpt
description: Filter for "HellionChat" or "ChatTwo" if the log is huge
description: Filter for "HellionChat" if the log is huge
render: text
- type: checkboxes
+2 -2
View File
@@ -53,7 +53,7 @@ new commands, new translations, removed behaviour. If none, write
bump and is it covered by the existing migration tests?
- Does this change the schema in MessageStore?
- Does this change the repo.json or HellionChat.yaml manifest fields?
- Does this affect the upstream cherry-pick path? See UPSTREAM_SYNC.md.
- Does this affect the upstream cherry-pick path? See docs/UPSTREAM_SYNC.md.
-->
## Checklist
@@ -67,6 +67,6 @@ new commands, new translations, removed behaviour. If none, write
- [ ] I updated the README, in-plugin strings or documentation if my
change is user-visible.
- [ ] I did not include any AI-generated code without disclosing it
in the PR description (see [AI_DISCLOSURE.md](../AI_DISCLOSURE.md)).
in the PR description (see [AI_DISCLOSURE.md](../docs/AI_DISCLOSURE.md)).
- [ ] I confirm my contribution is released under the
[EUPL-1.2](../LICENSE).
+1 -1
View File
@@ -5,7 +5,7 @@ updates:
# noise down while still catching transitive security advisories within
# a few days of disclosure.
- package-ecosystem: nuget
directory: /ChatTwo
directory: /HellionChat
schedule:
interval: weekly
day: monday
+12
View File
@@ -0,0 +1,12 @@
---
subtitle: "Theme Foundation"
versionsnatur: "Major-UI-Cycle"
---
- Theme-Engine mit fünf Built-In-Themes: Hellion Arctic (Default), Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove
- Settings öffnet jetzt eine Card-Grid-Übersicht — Klick auf eine Card führt in den Detail-View, Breadcrumb und ESC zurück zur Übersicht
- Themes-Tab mit Mini-Mockup pro Theme, Live-Switch beim Klick
- Eigene Themes als JSON in `pluginConfigs/HellionChat/themes/` — Beispiel-Vorlage wird beim ersten Start automatisch abgelegt
- Optional pro Theme eigene Chat-Channel-Farben mit Übernehmen/Behalten-Banner — niemals automatisch überschrieben
- Plugin-Icon zum Hellion-Forge-Hammer gewechselt
- Migration v13 → v14: alle User landen auf Hellion Arctic. Wer den Upstream-Look will, wählt Chat 2 Klassik in Settings → Themes
- Anleitung zum Schreiben eigener Themes: `docs/THEME-AUTHORING.md`
+1 -1
View File
@@ -15,7 +15,7 @@ Dalamud main plugin repo. To install:
- [README](https://github.com/JonKazama-Hellion/HellionChat/blob/main/README.md) — features, architecture, build
- [Privacy notice](https://github.com/JonKazama-Hellion/HellionChat/blob/main/PRIVACY.md) — what the plugin stores and sends
- [Third-party notices](https://github.com/JonKazama-Hellion/HellionChat/blob/main/THIRD_PARTY_NOTICES.md) — dependencies and licences
- [Third-party notices](https://github.com/JonKazama-Hellion/HellionChat/blob/main/docs/THIRD_PARTY_NOTICES.md) — dependencies and licences
- [Security policy](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SECURITY.md) — vulnerability reporting
- [Support](https://github.com/JonKazama-Hellion/HellionChat/blob/main/SUPPORT.md) — bug reports, questions, contact paths
+4 -4
View File
@@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v6
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
@@ -42,15 +42,15 @@ jobs:
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
- name: Restore
run: dotnet restore ChatTwo/ChatTwo.csproj
run: dotnet restore HellionChat/HellionChat.csproj
- name: Build (Release)
run: dotnet build ChatTwo/ChatTwo.csproj --configuration Release --no-restore
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
- name: Upload build output
uses: actions/upload-artifact@v7
with:
name: HellionChat-build-${{ github.run_number }}
path: ChatTwo/bin/Release/**/HellionChat/**
path: HellionChat/bin/Release/**/HellionChat/**
if-no-files-found: warn
retention-days: 14
+9 -9
View File
@@ -39,10 +39,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: 10.0.x
@@ -55,20 +55,20 @@ jobs:
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: csharp
build-mode: manual
queries: security-extended
- name: Restore
run: dotnet restore ChatTwo/ChatTwo.csproj
run: dotnet restore HellionChat/HellionChat.csproj
- name: Build (Release)
run: dotnet build ChatTwo/ChatTwo.csproj --configuration Release --no-restore
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: /language:csharp
@@ -79,15 +79,15 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: actions
build-mode: none
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: /language:actions
+6 -6
View File
@@ -8,7 +8,7 @@ name: Release
# - the tag name (filtered by on.tags = v*, validated again at runtime
# against ^v\d+\.\d+\.\d+$ before being used in any string)
# All other values are either repo-controlled (paths under
# ChatTwo/bin/Release derived from Get-ChildItem) or pinned URLs to
# HellionChat/bin/Release derived from Get-ChildItem) or pinned URLs to
# goatcorp / GitHub. Nothing from a webhook event payload (issue/PR
# titles, commit messages, etc.) flows into a run-step.
@@ -47,7 +47,7 @@ jobs:
ref: ${{ github.event.inputs.tag || github.ref }}
- name: Setup .NET 10
uses: actions/setup-dotnet@v4
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
@@ -60,16 +60,16 @@ jobs:
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
- name: Build (Release)
run: dotnet build ChatTwo/ChatTwo.csproj --configuration Release
run: dotnet build HellionChat/HellionChat.csproj --configuration Release
- name: Locate latest.zip
id: locate
shell: pwsh
run: |
$zip = Get-ChildItem -Path ChatTwo\bin\Release -Recurse -Filter latest.zip | Select-Object -First 1
$zip = Get-ChildItem -Path HellionChat\bin\Release -Recurse -Filter latest.zip | Select-Object -First 1
if (-not $zip)
{
throw "latest.zip not found under ChatTwo\bin\Release"
throw "latest.zip not found under HellionChat\bin\Release"
}
Write-Host "Found: $($zip.FullName)"
"path=$($zip.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
@@ -100,7 +100,7 @@ jobs:
}
$version = $tag.Substring(1)
$yamlPath = "ChatTwo/HellionChat.yaml"
$yamlPath = "HellionChat/HellionChat.yaml"
$raw = Get-Content -Path $yamlPath -Raw
$marker = "changelog: |-"
+4
View File
@@ -11,6 +11,10 @@
.vscode/
scripts/
# Local test project (stays out of the published plugin repo;
# pure-function safety net for refactor cycles)
HellionChat.Tests/
# Packaging
pack/
+10 -11
View File
@@ -10,7 +10,7 @@ what I am not, and how to make a contribution land smoothly.
- Read the [README](README.md) so you understand the scope: this is a
privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally
removes the upstream webinterface and ships smaller defaults.
- Read [UPSTREAM_SYNC.md](UPSTREAM_SYNC.md). Cherry-picks from upstream
- Read [UPSTREAM_SYNC.md](docs/UPSTREAM_SYNC.md). Cherry-picks from upstream
Chat 2 are selective and conscious; not everything that lands there
belongs here.
- Read [SECURITY.md](SECURITY.md). Anything security-sensitive goes
@@ -22,7 +22,7 @@ what I am not, and how to make a contribution land smoothly.
- Bug fixes for behaviour documented in the README, the in-plugin
settings or the changelog.
- Translation contributions for Hellion-specific strings via direct
pull requests against `ChatTwo/Resources/HellionStrings.*.resx`.
pull requests against `HellionChat/Resources/HellionStrings.*.resx`.
Translations for the upstream Chat 2 strings (`Language.*.resx`) are
not handled here; they go through the upstream Chat 2 project.
- Documentation improvements (README, comments, this file).
@@ -40,7 +40,7 @@ what I am not, and how to make a contribution land smoothly.
They make selective upstream cherry-picks much harder and the
maintenance cost outweighs the benefit for a one-person project.
- AI-generated code dropped in without disclosure or human review. See
[AI_DISCLOSURE.md](AI_DISCLOSURE.md) for how I handle AI assistance
[AI_DISCLOSURE.md](docs/AI_DISCLOSURE.md) for how I handle AI assistance
on my side; I expect comparable transparency from contributors.
If you are unsure whether an idea fits, open a feature-request issue
@@ -60,7 +60,7 @@ proposal than to a finished pull request.
easier to review than one big one. Squash-on-merge happens at the
PR level if needed.
5. If your change touches user-visible behaviour, update the README
and/or the changelog block in `ChatTwo/HellionChat.yaml` and
and/or the changelog block in `HellionChat/HellionChat.yaml` and
`repo.json` for the next version. I bump the version number myself
at release time, so you do not need to.
6. Open the pull request against `main`. The PR template will ask
@@ -79,13 +79,12 @@ locally you need:
```
dotnet restore
dotnet build ChatTwo.sln -c Release
dotnet test ChatTwo.sln -c Release
dotnet build HellionChat.sln -c Release
```
The test project is `ChatTwo.Tests`. New behaviour should come with a
test where the existing test infrastructure makes that practical
(privacy filter, configuration migration, message store).
Tests are not part of the current `HellionChat.sln`. If you add a test
project, point it at the relevant subsystems (privacy filter,
configuration migration, message store) and mention it in the PR.
For a smoke test in-game: build, copy the output into your Dalamud
`devPlugins/HellionChat/` directory and load it through `/xlplugins`.
@@ -114,11 +113,11 @@ There is no separate CLA.
## Translations
Hellion-specific strings live in `ChatTwo/Resources/HellionStrings.resx`
Hellion-specific strings live in `HellionChat/Resources/HellionStrings.resx`
(English source) and `HellionStrings.<lang>.resx` (per-language).
Translations are accepted as direct pull requests against those files.
The upstream Chat 2 strings in `ChatTwo/Resources/Language.*.resx` are
The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx` are
**not** translated in this repository. They are owned by the upstream
Chat 2 project and synced in via cherry-pick. Please contribute
upstream-string translations to
+1 -1
View File
@@ -1,6 +1,6 @@
HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
Copyright (c) 2024-2025 Infiziert90 (Infi) and Anna Clemens (ascclemens)
Copyright (c) 2022-2026 Infiziert90 (Infi) and Anna Clemens (ascclemens)
Original ChatTwo authors and copyright holders of the upstream
plugin this fork is built on. Their work covers the message store,
the channel filtering, the sidebar tab system, the FFXIV chat
-22
View File
@@ -1,22 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatTwo", "ChatTwo\ChatTwo.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatTwo.Tests", "ChatTwo.Tests\ChatTwo.Tests.csproj", "{A9FE423A-240C-4EDA-ACC6-21474B562128}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = Debug|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.ActiveCfg = Release|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = Release|Any CPU
{A9FE423A-240C-4EDA-ACC6-21474B562128}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A9FE423A-240C-4EDA-ACC6-21474B562128}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9FE423A-240C-4EDA-ACC6-21474B562128}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9FE423A-240C-4EDA-ACC6-21474B562128}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
-143
View File
@@ -1,143 +0,0 @@
name: Hellion Chat
author: JonKazama-Hellion
punchline: Chat 2 with privacy controls aligned to EU, US and JP rules
description: |-
Hellion Chat is built on top of Chat 2 with one removal and a stack
of privacy controls on top. Tabs, channel filters, RGB colours,
emotes, screenshot mode, IPC integration and the chat replacement
window itself work the same. The optional webinterface that Chat 2
ships is intentionally not part of this fork because it serves a
different use case from the smaller default footprint Hellion Chat
is built around.
On top of that, Hellion Chat adds privacy and data-handling controls
designed to align with the modern data protection rules that apply
across the EU, the United States and Japan. By default only your own
conversations are stored; messages from strangers, NPCs and system
spam stay out of the database. Retention windows are configurable per
channel, history can be wiped retroactively, and stored data can be
exported on demand.
Key additions on top of Chat 2:
- Channel whitelist with a Privacy-First default
- Per-channel retention with a daily background sweep
- Retroactive cleanup with a Ctrl+Shift confirm
- Export to Markdown, JSON or CSV
- First-run wizard with three preset profiles (Privacy-First, Casual,
Full History)
- Bilingual UI (English and German) with live language switching
- Independent plugin state — own config file and database directory,
so Hellion Chat does not share state with the upstream plugin
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
Modding & support: join the Hellion Forge Discord at
https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and
other Hellion Online Media plugins/tools.
repo_url: https://github.com/JonKazama-Hellion/HellionChat
accepts_feedback: true
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/ChatTwo/images/icon.png
image_urls:
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/ChatTwo/images/chatWindow.png
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/ChatTwo/images/withSimpleTweaks.png
tags:
- Social
- UI
- Chat
- Replacement
- Privacy
changelog: |-
**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**
- Pop-out button now visible in the chat header (no more hunting
through the right-click menu)
- One-time hint banner explains pop-out tabs and the right-click
shortcut
- New setting: open new /tell tabs directly as pop-out windows
(Settings → Chat → Auto-Tell-Tabs)
- Pop-out input is now enabled by default — closing a pop-out still
returns the tab to the sidebar
- Bugfix: dropping or logging out with an LRU/popped auto-tell tab
now also closes its pop-out window (no more ghost windows)
- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out
hint banner was visible (also fixed retroactively for the v0.6.0
banner inside pop-outs)
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.6.0 — UX Polish: Pop-Out Input + Colour Presets**
Two opt-in UX features land in the same release. Existing users see
no change unless they enable the new toggles.
Pop-out input bar:
- New global master switch in Settings → Window → Frame: "Enable input
in pop-outs". Default OFF so existing behaviour is preserved
- When enabled, every pop-out window grows a compact input bar at the
bottom (channel-coloured icon button left, text input right). The
auto-translate picker is intentionally not part of the compact bar
in v0.6.0 — typical pop-out workflows (FC greeter, club hostess)
rarely need it there
- Each pop-out keeps an independent text buffer and history cursor;
channel changes still apply globally because that is how the FFXIV
channel API works
- Up/Down navigates a shared input history singleton across the main
window and every open pop-out
- First pop-out opening after the upgrade shows a one-time hint
banner pointing users to the new toggle
Chat colour presets:
- Seven built-in presets above the per-channel colour list in
Settings → Appearance → Colours: ChatTwo Default, High-Contrast,
Pastell, Dark-Mode-Tuned, Hellion (brand-coloured, blue/orange
Arctic Cyan + Ember Glow palette from the Hellion Online Media
branding spec), plus two bonus mood presets — Night Blue (royal
blue, classic-cool) and Indigo Violet (royal violet, glitter-mystic)
- Apply is immediate and overwrites the channels covered by the
preset; battle-channel colours are left alone so combat tuning
stays intact
Configuration migrates from v10 to v11 with a diagnostic log entry;
no data is reset. Bilingual (English/German) for both new sections.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.5.4 — WrapText hardening**
Replaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with
Span- and index-based control flow. Closes the persistent CodeQL
Critical alert "unvalidated local pointer arithmetic" that kept
re-firing on every shape of the previous fix.
Hardening:
- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount
via ArrayPool, validates the actual encoded length against that
ceiling, and threads the rest of the algorithm through int offsets
instead of raw byte pointers
- Pointer arithmetic only happens inside two small private helpers
(CalcWordWrap and DrawText) that take the pinned base pointer plus
int offsets sourced from the plugin's own logic, not from any
virtual-method return
- Added a 16 KiB upper bound on the buffer rent to prevent a
pathological input from triggering an unbounded ArrayPool allocation
No user-visible behaviour change. Word-wrap output is byte-identical
to v0.5.3.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.5.3 — Pointer arithmetic hardening**
Closed CodeQL Critical alert in ImGuiUtil.WrapText by validating the
encoded byte buffer length via GetByteCount before pointer
arithmetic. Single-fix patch on top of v0.5.2.
---
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
-1
View File
@@ -1 +0,0 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ChatTwo.Tests")]
-230
View File
@@ -1,230 +0,0 @@
using ChatTwo.Util;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
namespace ChatTwo.Ui;
/// <summary>
/// ImGui style override for Hellion Chat. Industrial HUD palette with three
/// distinct accents — cyan-teal as the primary action color, industrial
/// amber for active state highlights, slate-violet for title bars and
/// active tabs — on a deep-slate frame background with steel borders.
///
/// Two entry points:
/// Push — local color stack, scoped via using-block. Use inside
/// Hellion-only surfaces (Privacy tab, first-run wizard).
/// PushGlobal — full color + style variable stack. Pushed once per frame
/// in Plugin.Draw so every Hellion-rendered window inherits
/// the look. Cheap to pop because ImGui keeps its own stack.
/// </summary>
internal static class HellionStyle
{
// Encoded as 0xRRGGBBAA, matching ChatTwo convention (see Settings.cs
// Ko-fi buttons). RgbaToAbgr handles the byte swap to the format ImGui
// expects. Hex values are sourced from the Hellion Online Media brand
// guide ("Arctic Cyan + Ember Glow", BRANDING.md in the website repo).
// Primary — Arctic Cyan, used for every interactive control (buttons,
// checks, sliders, separators when hovered). Three brand stages plus a
// hover that lifts to brand-color-light and a press that drops to
// brand-color-dark.
private const uint PrimaryRgba = 0x00BED2FF; // brand-color
private const uint PrimaryHoverRgba = 0x4DD9E8FF; // brand-color-light
private const uint PrimaryActiveRgba = 0x0097A7FF; // brand-color-dark
// Identity — brand-color-dark teal for window title bars and the
// active tab. Sits visibly below the primary cyan on buttons so the
// user sees "where am I" (deep teal) versus "what can I click"
// (brand cyan) without leaving the cyan family.
private const uint IdentityRgba = 0x0097A7FF; // brand-color-dark
private const uint IdentityHoverRgba = 0x4DD9E8FF; // brand-color-light
private const uint IdentityDeepRgba = 0x005670FF; // dimmer teal for unfocused-active tab
// Accent — Ember Orange for warm highlights on grips and scrollbar
// pulls. Replaces the previous industrial amber so the plugin matches
// the website's CTA palette. AccentActive is reserved for any future
// pressed-state on accent surfaces; the current slots only need
// AccentRgba and AccentHoverRgba.
private const uint AccentRgba = 0xF97316FF; // accent-color
private const uint AccentHoverRgba = 0xFB923CFF; // accent-color-light
// Surfaces — Hellion brand background ladder. Window darkest, frame
// hover ladder climbs into surface tones. Matches the website's
// background / background-medium / background-light / surface vars.
private const uint WindowBgRgba = 0x070B12FF; // background
private const uint ChildBgRgba = 0x0C1220FF; // background-medium
private const uint PopupBgRgba = 0x0C1220FF; // background-medium
private const uint FrameBgRgba = 0x141E30FF; // background-light
private const uint FrameBgHoverRgba = 0x1A2538FF; // surface
private const uint FrameBgActiveRgba = 0x22303FFF; // surface-hover
// Cyan-tinted border — matches website --border-brand (cyan @ 40% α).
private const uint BorderRgba = 0x00BED266;
private const uint BorderShadowRgba = 0x00000000;
// Headers / collapsing-headers / tree nodes / selectables — same
// surface ladder as frames so panels feel consistent.
private const uint HeaderRgba = 0x141E30FF;
private const uint HeaderHoverRgba = 0x1A2538FF;
private const uint HeaderActiveRgba = 0x22303FFF;
// Title bars — Identity teal on active so the focused window reads
// as "yours" without using accent or primary slots.
private const uint TitleBgRgba = 0x070B12FF;
private const uint TitleBgActiveRgba = IdentityRgba;
private const uint TitleBgCollapsedRgba = 0x05080EFF;
// Tabs — neutral inactive, Identity-light on hover, Identity teal on
// active. Unfocused-active uses the deeper Identity stage so an
// unfocused window's active tab still reads but does not pull focus.
private const uint TabRgba = 0x141E30FF;
private const uint TabHoveredRgba = IdentityHoverRgba;
private const uint TabActiveRgba = IdentityRgba;
private const uint TabUnfocusedRgba = 0x0C1220FF;
private const uint TabUnfocusedActiveRgba = IdentityDeepRgba;
// Scrollbar — Ember on grab so the pull stands out without competing
// with the cyan action buttons. Idle grab is a subtle surface tone,
// hover/active climb into accent.
private const uint ScrollbarBgRgba = 0x070B12FF;
private const uint ScrollbarGrabRgba = 0x22303FFF; // surface-hover
private const uint ScrollbarGrabHoveredRgba = AccentHoverRgba;
private const uint ScrollbarGrabActiveRgba = AccentRgba;
// Resize grip — same Ember treatment as the scrollbar.
private const uint ResizeGripRgba = 0x141E30FF;
private const uint ResizeGripHoveredRgba = AccentHoverRgba;
private const uint ResizeGripActiveRgba = AccentRgba;
// Separator and check mark / slider follow the primary cyan.
/// <summary>
/// Local color stack for Hellion-only surfaces. Cheap. Use inside a
/// `using var _ = HellionStyle.Push();` block.
/// </summary>
internal static IDisposable Push()
{
var stack = new StackHandle();
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
stack.PushColor(ImGuiCol.Border, BorderRgba);
stack.PushColor(ImGuiCol.Header, HeaderRgba);
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
return stack;
}
/// <summary>
/// Global color and style-variable stack pushed once per frame in
/// Plugin.Draw. Covers every ImGui surface the plugin renders so the
/// Hellion look is consistent across upstream and Hellion tabs.
/// </summary>
/// <param name="windowOpacity">Window background alpha (0.51.0). Lower
/// values let the game shine through the plugin panes.</param>
internal static IDisposable PushGlobal(float windowOpacity = 1.0f)
{
var stack = new StackHandle();
// Mix the configured opacity into both the outer window and the
// inner content child backgrounds — without ChildBg following the
// slider the chat log stays opaque inside even when the user
// wants to see the game behind it during combat. Form fields and
// popups (FrameBg, PopupBg) still stay opaque so input is readable.
var alphaByte = (uint)Math.Clamp((int)(windowOpacity * 255f), 0x55, 0xFF);
var windowBgWithAlpha = (WindowBgRgba & 0xFFFFFF00u) | alphaByte;
var childBgWithAlpha = (ChildBgRgba & 0xFFFFFF00u) | alphaByte;
// Layout — geometric edges, modest rounding, single-pixel borders.
stack.PushStyleVar(ImGuiStyleVar.WindowRounding, 4f);
stack.PushStyleVar(ImGuiStyleVar.ChildRounding, 3f);
stack.PushStyleVar(ImGuiStyleVar.PopupRounding, 3f);
stack.PushStyleVar(ImGuiStyleVar.FrameRounding, 2f);
stack.PushStyleVar(ImGuiStyleVar.GrabRounding, 2f);
stack.PushStyleVar(ImGuiStyleVar.TabRounding, 2f);
stack.PushStyleVar(ImGuiStyleVar.ScrollbarRounding, 2f);
stack.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 1f);
stack.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
// Surfaces.
stack.PushColor(ImGuiCol.WindowBg, windowBgWithAlpha);
stack.PushColor(ImGuiCol.ChildBg, childBgWithAlpha);
stack.PushColor(ImGuiCol.PopupBg, PopupBgRgba);
stack.PushColor(ImGuiCol.Border, BorderRgba);
stack.PushColor(ImGuiCol.BorderShadow, BorderShadowRgba);
// Frames (input fields, combos, sliders).
stack.PushColor(ImGuiCol.FrameBg, FrameBgRgba);
stack.PushColor(ImGuiCol.FrameBgHovered, FrameBgHoverRgba);
stack.PushColor(ImGuiCol.FrameBgActive, FrameBgActiveRgba);
// Title bars — tertiary identity on active.
stack.PushColor(ImGuiCol.TitleBg, TitleBgRgba);
stack.PushColor(ImGuiCol.TitleBgActive, TitleBgActiveRgba);
stack.PushColor(ImGuiCol.TitleBgCollapsed, TitleBgCollapsedRgba);
// Buttons — primary cyan.
stack.PushColor(ImGuiCol.Button, PrimaryRgba);
stack.PushColor(ImGuiCol.ButtonHovered, PrimaryHoverRgba);
stack.PushColor(ImGuiCol.ButtonActive, PrimaryActiveRgba);
// Headers / selectables — slate with subtle steps.
stack.PushColor(ImGuiCol.Header, HeaderRgba);
stack.PushColor(ImGuiCol.HeaderHovered, HeaderHoverRgba);
stack.PushColor(ImGuiCol.HeaderActive, HeaderActiveRgba);
// Tabs — tertiary identity for the active tab.
stack.PushColor(ImGuiCol.Tab, TabRgba);
stack.PushColor(ImGuiCol.TabHovered, TabHoveredRgba);
stack.PushColor(ImGuiCol.TabActive, TabActiveRgba);
stack.PushColor(ImGuiCol.TabUnfocused, TabUnfocusedRgba);
stack.PushColor(ImGuiCol.TabUnfocusedActive, TabUnfocusedActiveRgba);
// Scrollbar.
stack.PushColor(ImGuiCol.ScrollbarBg, ScrollbarBgRgba);
stack.PushColor(ImGuiCol.ScrollbarGrab, ScrollbarGrabRgba);
stack.PushColor(ImGuiCol.ScrollbarGrabHovered, ScrollbarGrabHoveredRgba);
stack.PushColor(ImGuiCol.ScrollbarGrabActive, ScrollbarGrabActiveRgba);
// Resize grip — secondary amber on active.
stack.PushColor(ImGuiCol.ResizeGrip, ResizeGripRgba);
stack.PushColor(ImGuiCol.ResizeGripHovered, ResizeGripHoveredRgba);
stack.PushColor(ImGuiCol.ResizeGripActive, ResizeGripActiveRgba);
// Check mark + slider grab — primary cyan.
stack.PushColor(ImGuiCol.CheckMark, PrimaryRgba);
stack.PushColor(ImGuiCol.SliderGrab, PrimaryRgba);
stack.PushColor(ImGuiCol.SliderGrabActive, PrimaryHoverRgba);
// Separator — primary cyan when hovered/active so the eye
// immediately sees that splitters are interactive.
stack.PushColor(ImGuiCol.Separator, BorderRgba);
stack.PushColor(ImGuiCol.SeparatorHovered, PrimaryHoverRgba);
stack.PushColor(ImGuiCol.SeparatorActive, PrimaryRgba);
return stack;
}
private sealed class StackHandle : IDisposable
{
private readonly List<IDisposable> _items = new(64);
internal void PushColor(ImGuiCol slot, uint rgba)
=> _items.Add(ImRaii.PushColor(slot, ColourUtil.RgbaToAbgr(rgba)));
internal void PushStyleVar(ImGuiStyleVar var, float value)
=> _items.Add(ImRaii.PushStyle(var, value));
public void Dispose()
{
for (var i = _items.Count - 1; i >= 0; i--)
_items[i].Dispose();
_items.Clear();
}
}
}
-26
View File
@@ -1,26 +0,0 @@
using System.Text;
namespace ChatTwo.Util;
public static class MemoryUtil
{
public static unsafe void PrintMemoryArea(nint address, int length)
{
var ptr = (byte*)address;
var str = new StringBuilder("\n");
for(var i = 0; i < length; i++)
{
str.Append($"{ptr![i]:X02}");
if (i == 0)
continue;
if ((i+1) % 16 == 0)
str.Append('\n');
else if ((i+1) % 4 == 0)
str.Append(' ');
}
Plugin.Log.Information(str.ToString());
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

+31
View File
@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HellionChat", "HellionChat\HellionChat.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = Debug|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x64.ActiveCfg = Debug|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x64.Build.0 = Debug|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x86.ActiveCfg = Debug|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|x86.Build.0 = Debug|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.ActiveCfg = Release|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = Release|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x64.ActiveCfg = Release|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x64.Build.0 = Release|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x86.ActiveCfg = Release|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
@@ -1,14 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using ChatTwo.Code;
using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources;
using ChatTwo.Util;
using HellionChat.Code;
using HellionChat.GameFunctions.Types;
using HellionChat.Resources;
using HellionChat.Util;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
namespace ChatTwo;
namespace HellionChat;
// Hellion Chat — Auto-Tell-Tabs.
//
+27
View File
@@ -0,0 +1,27 @@
using System.Linq;
using HellionChat.Resources;
using Dalamud.Plugin;
namespace HellionChat;
internal static class ChatTwoConflictDetector
{
private const string UpstreamInternalName = "ChatTwo";
public static void ThrowIfChatTwoIsLoaded(IDalamudPluginInterface pluginInterface)
{
var conflict = pluginInterface.InstalledPlugins
.FirstOrDefault(p =>
p.InternalName == UpstreamInternalName &&
p.IsLoaded);
if (conflict is null)
return;
var message = HellionStrings.ChatTwoConflictTitle + "\n\n" +
HellionStrings.ChatTwoConflictBody + "\n\n" +
HellionStrings.ChatTwoConflictAction;
throw new System.InvalidOperationException(message);
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
using ChatTwo.Code;
using HellionChat.Code;
using Dalamud.Game.Text.SeStringHandling;
using MessagePack;
namespace ChatTwo;
namespace HellionChat;
[Union(0, typeof(TextChunk))]
[Union(1, typeof(IconChunk))]
@@ -1,6 +1,6 @@
using Dalamud.Game.Text;
namespace ChatTwo.Code;
namespace HellionChat.Code;
public class ChatCode
{
@@ -91,13 +91,10 @@ public class ChatCode
public override bool Equals(object? obj)
{
if (obj == null)
return false;
if (obj is not ChatCode code)
return false;
return GetHashCode() == code.GetHashCode();
return Type == code.Type && Source == code.Source && Target == code.Target;
}
public override int GetHashCode()
@@ -1,6 +1,6 @@
using Dalamud.Game.Text;
namespace ChatTwo.Code;
namespace HellionChat.Code;
[Flags]
public enum ChatSource : ushort
@@ -1,6 +1,6 @@
using ChatTwo.Resources;
using HellionChat.Resources;
namespace ChatTwo.Code;
namespace HellionChat.Code;
internal static class ChatSourceExt
{
@@ -1,4 +1,4 @@
namespace ChatTwo.Code;
namespace HellionChat.Code;
public enum ChatType : ushort
{
@@ -1,8 +1,8 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using HellionChat.Resources;
using HellionChat.Util;
using Dalamud.Game.Config;
namespace ChatTwo.Code;
namespace HellionChat.Code;
internal static class ChatTypeExt
{
@@ -1,4 +1,4 @@
namespace ChatTwo.Code;
namespace HellionChat.Code;
public enum InputChannel : uint
{
@@ -1,6 +1,6 @@
using Lumina.Excel.Sheets;
namespace ChatTwo.Code;
namespace HellionChat.Code;
internal static class InputChannelExt
{
@@ -1,6 +1,6 @@
using Dalamud.Game.Command;
namespace ChatTwo;
namespace HellionChat;
internal sealed class Commands : IDisposable
{
@@ -1,8 +1,8 @@
using System.Collections;
using ChatTwo.Code;
using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources;
using ChatTwo.Util;
using HellionChat.Code;
using HellionChat.GameFunctions.Types;
using HellionChat.Resources;
using HellionChat.Util;
using Dalamud;
using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys;
@@ -10,7 +10,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Bindings.ImGui;
namespace ChatTwo;
namespace HellionChat;
[Serializable]
public class ConfigKeyBind
@@ -34,10 +34,23 @@ public class ConfigKeyBind
[Serializable]
public class Configuration : IPluginConfiguration
{
private const int LatestVersion = 12;
private const int LatestVersion = 14;
public int Version { get; set; } = LatestVersion;
// v1.1.0 — Theme-Engine. Slug-basiert; ThemeRegistry liefert das Objekt.
public string Theme = "hellion-arctic";
// v1.1.0 — Globale Window-Opacity, theme-übergreifend. Migration aus
// HellionThemeWindowOpacity beim Bump v13 → v14.
public float WindowOpacity = 0.85f;
// v1.1.0 — Felder für künftige UI-Toggles (v1.2.0 / v1.3.0). Werden
// vorab angelegt, damit später keine Migration nötig ist.
public bool ReduceMotion;
public bool UseCompactDensity;
public bool ShowThemeQuickPicker;
// Hellion Chat — Privacy filter (DSGVO Art. 25 Privacy by Default).
// Master-switch defaults to true; set false to restore upstream behavior.
public bool PrivacyFilterEnabled = true;
@@ -70,12 +83,14 @@ public class Configuration : IPluginConfiguration
// Hellion Chat global ImGui theme — applied to every plugin window in
// Plugin.Draw. Default ON; users who prefer the upstream Dalamud look
// can flip this off in the Privacy tab.
[Obsolete("Replaced by Theme slug + WindowOpacity in v14")]
public bool HellionThemeEnabled = true;
// Window background opacity, 0.51.0. Lower values make the plugin
// panes more glass-like so the game shines through. Default 0.5
// matches the maintainer's daily-driver preference; users who want
// a less translucent look bump it up in Aussehen → Theme.
[Obsolete("Replaced by WindowOpacity in v14")]
public float HellionThemeWindowOpacity = 0.5f;
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
@@ -145,6 +160,7 @@ public class Configuration : IPluginConfiguration
public bool HideWhenUiHidden = true;
public bool HideInLoadingScreens;
public bool HideInBattle;
public bool HideInNewGamePlusMenu;
public bool HideWhenInactive;
public int InactivityHideTimeout = 10;
public bool InactivityHideActiveDuringBattle = true;
@@ -221,6 +237,7 @@ public class Configuration : IPluginConfiguration
public float TooltipOffset;
public float WindowAlpha = 100f;
public Dictionary<ChatType, uint> ChatColours = new();
public bool ColorSelectedInputChannelButton = true;
public List<Tab> Tabs = [];
public bool OverrideStyle;
@@ -241,6 +258,7 @@ public class Configuration : IPluginConfiguration
HideWhenUiHidden = other.HideWhenUiHidden;
HideInLoadingScreens = other.HideInLoadingScreens;
HideInBattle = other.HideInBattle;
HideInNewGamePlusMenu = other.HideInNewGamePlusMenu;
HideWhenInactive = other.HideWhenInactive;
InactivityHideTimeout = other.InactivityHideTimeout;
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
@@ -276,7 +294,10 @@ public class Configuration : IPluginConfiguration
MaxLinesToRender = other.MaxLinesToRender;
Use24HourClock = other.Use24HourClock;
ShowEmotes = other.ShowEmotes;
BlockedEmotes = other.BlockedEmotes;
// Deep-copy the set so the live and mutable Configuration instances don't share state
// — a HashSet reference assignment would cause edits in the settings window to leak
// into the live config before the user clicks Save.
BlockedEmotes = new HashSet<string>(other.BlockedEmotes);
FontsEnabled = other.FontsEnabled;
ItalicEnabled = other.ItalicEnabled;
ExtraGlyphRanges = other.ExtraGlyphRanges;
@@ -288,6 +309,7 @@ public class Configuration : IPluginConfiguration
TooltipOffset = other.TooltipOffset;
WindowAlpha = other.WindowAlpha;
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore
// never present in a disk-loaded copy. Keep the live temp tabs of
@@ -314,10 +336,19 @@ public class Configuration : IPluginConfiguration
RetentionLastRunAt = other.RetentionLastRunAt;
FirstRunCompleted = other.FirstRunCompleted;
#pragma warning disable CS0612, CS0618 // Obsolete-Felder bleiben bis v1.2.0 als JSON-Safety-Net erhalten
HellionThemeEnabled = other.HellionThemeEnabled;
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
#pragma warning restore CS0612, CS0618
UseHellionFont = other.UseHellionFont;
// v1.1.0 theme engine fields
Theme = other.Theme;
WindowOpacity = other.WindowOpacity;
ReduceMotion = other.ReduceMotion;
UseCompactDensity = other.UseCompactDensity;
ShowThemeQuickPicker = other.ShowThemeQuickPicker;
EnableAutoTellTabs = other.EnableAutoTellTabs;
AutoTellTabsLimit = other.AutoTellTabsLimit;
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
@@ -8,7 +8,7 @@ using Dalamud.Bindings.ImGui;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace ChatTwo;
namespace HellionChat;
public static class EmoteCache
{
@@ -66,16 +66,29 @@ public static class EmoteCache
public static string[] SortedCodeArray = [];
// Plugin-scoped cancellation source for in-flight emote loads. Dispose
// cancels every running download/texture-create so the workers don't
// touch a torn-down TextureProvider on plugin reload. Replaced with a
// fresh source on the next LoadData() call so a re-enable still works.
private static CancellationTokenSource Cts = new();
internal static CancellationToken Token => Cts.Token;
public static async Task LoadData()
{
if (State is not LoadingState.Unloaded)
return;
// Refresh the CTS in case Dispose was called and we're being re-enabled
// in the same process (Dalamud /xlplugins toggle).
if (Cts.IsCancellationRequested)
Cts = new CancellationTokenSource();
State = LoadingState.Loading;
var ct = Cts.Token;
try
{
var global = await Client.GetAsync(GlobalEmotes);
var globalList = await global.Content.ReadAsStringAsync();
var global = await Client.GetAsync(GlobalEmotes, ct);
var globalList = await global.Content.ReadAsStringAsync(ct);
foreach (var emote in JsonSerializer.Deserialize<Emote[]>(globalList)!)
if (!string.IsNullOrEmpty(emote.Code) && !NotWorking.Contains(emote.Code))
@@ -84,8 +97,8 @@ public static class EmoteCache
var lastId = string.Empty;
for (var i = 0; i < 15; i++)
{
var top = await Client.GetAsync(Top100Emotes.Format(BetterTTV, lastId));
var topList = await top.Content.ReadAsStringAsync();
var top = await Client.GetAsync(Top100Emotes.Format(BetterTTV, lastId), ct);
var topList = await top.Content.ReadAsStringAsync(ct);
var jsonList = JsonSerializer.Deserialize<List<Top100>>(topList)!;
// BetterTTV occasionally returns entries with a null Code; the
@@ -103,6 +116,12 @@ public static class EmoteCache
SortedCodeArray = Cache.Keys.Order().ToArray();
State = LoadingState.Done;
}
catch (OperationCanceledException)
{
// Plugin disposed while the cache was loading; leave State on
// Loading so a subsequent re-enable can re-issue LoadData with
// a fresh CTS (handled above).
}
catch (Exception ex)
{
// Reset to Unloaded so a later trigger (e.g. the user reopening
@@ -116,6 +135,10 @@ public static class EmoteCache
public static void Dispose()
{
// Cancel in-flight downloads / texture creates so the async-void
// Load methods bail out before they touch a disposed TextureProvider.
Cts.Cancel();
foreach (var emote in EmoteImages.Values)
emote.InnerDispose();
}
@@ -171,7 +194,7 @@ public static class EmoteCache
ImGui.Image(Texture!.Handle, size);
}
internal async Task<byte[]> LoadAsync(Emote emote)
internal async Task<byte[]> LoadAsync(Emote emote, CancellationToken ct)
{
// BetterTTV-supplied Id and ImageType are interpolated straight
// into the filename. HTTPS protects the wire, but a compromised
@@ -188,15 +211,15 @@ public static class EmoteCache
if (File.Exists(filePath))
{
RawData = await File.ReadAllBytesAsync(filePath);
RawData = await File.ReadAllBytesAsync(filePath, ct);
}
else
{
var content = await new HttpClient().GetAsync(EmotePath.Format(emote.Id));
RawData = await content.Content.ReadAsByteArrayAsync();
var content = await Client.GetAsync(EmotePath.Format(emote.Id), ct);
RawData = await content.Content.ReadAsByteArrayAsync(ct);
await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
stream.Write(RawData, 0, RawData.Length);
await stream.WriteAsync(RawData, ct);
}
return RawData;
@@ -209,21 +232,28 @@ public static class EmoteCache
{
public ImGuiEmote Prepare(Emote emote)
{
Task.Run(() => Load(emote));
var ct = EmoteCache.Token;
Task.Run(() => Load(emote, ct), ct);
return this;
}
private async void Load(Emote emote)
private async void Load(Emote emote, CancellationToken ct)
{
try
{
var image = await LoadAsync(emote);
var image = await LoadAsync(emote, ct);
if (image.Length <= 0)
return;
Texture = await Plugin.TextureProvider.CreateFromImageAsync(image);
ct.ThrowIfCancellationRequested();
Texture = await Plugin.TextureProvider.CreateFromImageAsync(image, cancellationToken: ct);
IsLoaded = true;
}
catch (OperationCanceledException)
{
// Plugin disposed mid-load; the EmoteImages entry is also
// being torn down, no extra cleanup needed.
}
catch (Exception ex)
{
Failed = true;
@@ -279,15 +309,16 @@ public static class EmoteCache
public ImGuiGif Prepare(Emote emote)
{
Task.Run(() => Load(emote));
var ct = EmoteCache.Token;
Task.Run(() => Load(emote, ct), ct);
return this;
}
private async void Load(Emote emote)
private async void Load(Emote emote, CancellationToken ct)
{
try
{
var image = await LoadAsync(emote);
var image = await LoadAsync(emote, ct);
if (image.Length <= 0)
return;
@@ -299,6 +330,8 @@ public static class EmoteCache
var frames = new List<(IDalamudTextureWrap Tex, float Delay)>();
foreach (var frame in img.Frames)
{
ct.ThrowIfCancellationRequested();
var delay = frame.Metadata.GetGifMetadata().FrameDelay / 100f;
// Follows the same pattern as browsers, anything under 0.02s delay will be rounded up to 0.1s
@@ -307,13 +340,21 @@ public static class EmoteCache
var buffer = new byte[4 * frame.Width * frame.Height];
frame.CopyPixelDataTo(buffer);
var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer);
var tex = await Plugin.TextureProvider.CreateFromRawAsync(RawImageSpecification.Rgba32(frame.Width, frame.Height), buffer, cancellationToken: ct);
frames.Add((tex, delay));
}
Frames = frames;
IsLoaded = true;
}
catch (OperationCanceledException)
{
// Plugin disposed mid-load; partial frames are released by
// InnerDispose on the next dispose pass.
foreach (var f in Frames)
f.Texture.Dispose();
Frames = [];
}
catch (Exception ex)
{
Failed = true;
@@ -1,8 +1,8 @@
using System.Globalization;
using System.Text;
using ChatTwo.Code;
using HellionChat.Code;
namespace ChatTwo.Export;
namespace HellionChat.Export;
internal enum ExportFormat
{
@@ -6,7 +6,7 @@ using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility;
using Dalamud.Bindings.ImGui;
namespace ChatTwo;
namespace HellionChat;
public class FontManager
{
@@ -18,8 +18,6 @@ public class FontManager
internal IFontHandle FontAwesome = null!;
internal readonly byte[] GameSymFont;
private ushort[] Ranges = [];
private ushort[] JpRange = [];
@@ -30,25 +28,6 @@ public class FontManager
36f, 40f, 45f, 46f, 68f, 90f,
];
public FontManager()
{
var filePath = Path.Combine(Plugin.Interface.ConfigDirectory.FullName, "FFXIV_Lodestone_SSF.ttf");
if (File.Exists(filePath))
{
GameSymFont = File.ReadAllBytes(filePath);
}
else
{
GameSymFont = new HttpClient().GetAsync("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")
.Result
.Content
.ReadAsByteArrayAsync()
.Result;
Dalamud.Utility.FilesystemUtil.WriteAllBytesSafe(filePath, GameSymFont);
}
}
/// <summary>
/// Backing bytes for the bundled Hellion font (Exo 2, OFL-1.1). Lazily
/// extracted from the assembly's manifest resources on first use; the
@@ -1,8 +1,8 @@
using System.Text;
using ChatTwo.Code;
using ChatTwo.GameFunctions.Types;
using ChatTwo.Resources;
using ChatTwo.Util;
using HellionChat.Code;
using HellionChat.GameFunctions.Types;
using HellionChat.Resources;
using HellionChat.Util;
using Dalamud.Game.Config;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking;
@@ -22,7 +22,7 @@ using Lumina.Text.ReadOnly;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
namespace ChatTwo.GameFunctions;
namespace HellionChat.GameFunctions;
internal sealed unsafe class Chat : IDisposable
{
@@ -252,7 +252,7 @@ internal sealed unsafe class Chat : IDisposable
{
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
worldId = agent->TellWorldId;
Plugin.Log.Debug($"Detected tell target '{playerName}'@{worldId}");
Plugin.Log.Debug($"Detected tell target '[redacted]'@{worldId}");
}
Plugin.CurrentTab.CurrentChannel = new UsedChannel
@@ -400,7 +400,9 @@ internal sealed unsafe class Chat : IDisposable
}
var idx = RotateLinkshell(currentIndex, rotate, channel == InputChannel.Linkshell1 ? ValidLinkshell : ValidCrossLinkshell);
return channel + idx;
// RotateLinkshell returns null when no valid linkshell is found within 8 iterations.
// Forward the null so the caller can keep the existing channel instead of crashing on nullable arithmetic.
return idx is null ? null : channel + idx.Value;
}
default:
return channel;
@@ -1,10 +1,10 @@
using System.Text;
using ChatTwo.Resources;
using HellionChat.Resources;
using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
namespace ChatTwo.GameFunctions;
namespace HellionChat.GameFunctions;
public unsafe class ChatBox
{
@@ -1,9 +1,9 @@
using ChatTwo.Util;
using HellionChat.Util;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
namespace ChatTwo.GameFunctions;
namespace HellionChat.GameFunctions;
internal sealed unsafe class Context
{
@@ -16,10 +16,12 @@ using Lumina.Excel;
using Lumina.Excel.Sheets;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
namespace ChatTwo.GameFunctions;
namespace HellionChat.GameFunctions;
internal unsafe class GameFunctions : IDisposable
{
internal const string NewGamePlusAddonName = "QuestRedo";
#region Hooks
[Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
@@ -243,15 +245,33 @@ internal unsafe class GameFunctions : IDisposable
vf0(agent, &result, &value, 0, 0);
}
private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(128);
private const int PlaceholderBufferSize = 128;
private readonly nint PlaceholderNamePtr = Marshal.AllocHGlobal(PlaceholderBufferSize);
private readonly string Placeholder = $"<{Guid.NewGuid():N}>";
private string? ReplacementName;
private nint ResolveTextCommandPlaceholderDetour(nint a1, byte* placeholderText, byte a3, byte a4)
{
// The detour is only invoked through the hook, so the hook should
// never be null here, but the nullable field declaration forces us
// to handle the theoretical race during teardown.
if (ResolveTextCommandPlaceholderHook is null)
return nint.Zero;
var placeholder = MemoryHelper.ReadStringNullTerminated((nint) placeholderText);
if (ReplacementName == null || placeholder != Placeholder)
return ResolveTextCommandPlaceholderHook!.Original(a1, placeholderText, a3, a4);
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
// The fixed buffer is 128 bytes; UTF-8 + null-terminator must fit.
// FFXIV player names plus an @World suffix should never approach this
// limit, but a malformed ReplacementName must not overflow the buffer.
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
if (byteCount >= PlaceholderBufferSize)
{
Plugin.Log.Warning($"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original.");
ReplacementName = null;
return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
}
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
ReplacementName = null;
@@ -1,16 +1,16 @@
using System.Numerics;
using ChatTwo.Code;
using ChatTwo.GameFunctions.Types;
using ChatTwo.Util;
using HellionChat.Code;
using HellionChat.GameFunctions.Types;
using HellionChat.Util;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Config;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using Dalamud.Bindings.ImGui;
using ModifierFlag = ChatTwo.GameFunctions.Types.ModifierFlag;
using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag;
namespace ChatTwo.GameFunctions;
namespace HellionChat.GameFunctions;
internal enum KeyboardSource {
Game,
@@ -1,10 +1,10 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using HellionChat.Resources;
using HellionChat.Util;
using Dalamud.Interface.ImGuiNotification;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info;
namespace ChatTwo.GameFunctions;
namespace HellionChat.GameFunctions;
internal static unsafe class Party
{
@@ -1,6 +1,6 @@
using ChatTwo.Code;
using HellionChat.Code;
namespace ChatTwo.GameFunctions.Types;
namespace HellionChat.GameFunctions.Types;
internal class ChannelSwitchInfo {
internal InputChannel? Channel { get; }
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types;
namespace HellionChat.GameFunctions.Types;
internal sealed class ChatActivatedArgs
{
@@ -1,6 +1,6 @@
using Dalamud.Game.ClientState.Keys;
namespace ChatTwo.GameFunctions.Types;
namespace HellionChat.GameFunctions.Types;
internal class Keybind
{
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types;
namespace HellionChat.GameFunctions.Types;
[Flags]
public enum ModifierFlag
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types;
namespace HellionChat.GameFunctions.Types;
internal enum RotateMode
{
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types;
namespace HellionChat.GameFunctions.Types;
internal sealed class TellHistoryInfo
{
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types;
namespace HellionChat.GameFunctions.Types;
public enum TellReason
{
@@ -1,7 +1,7 @@
using Dalamud.Game.ClientState.Objects.SubKinds;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
namespace ChatTwo.GameFunctions.Types;
namespace HellionChat.GameFunctions.Types;
[Serializable]
public class TellTarget
@@ -20,7 +20,7 @@ public class TellTarget
}
public bool IsSet()
=> Name.Length > 0 && World > 0;
=> !string.IsNullOrEmpty(Name) && World > 0;
public string ToWorldString()
=> Sheets.WorldSheet.TryGetRow(World, out var worldRow) ? worldRow.Name.ToString() : string.Empty;
@@ -30,6 +30,9 @@ public class TellTarget
public unsafe void FromTarget(IPlayerCharacter target)
{
if (target.Address == nint.Zero)
return;
Name = target.Name.TextValue;
World = target.HomeWorld.RowId;
ContentId = ((Character*)target.Address)->ContentId;
@@ -4,23 +4,44 @@
0.1.0 is our bootstrap release; the underlying Chat 2 base is
called out in the yaml changelog so users can see what it
derives from. -->
<Version>0.6.1</Version>
<Version>1.1.0</Version>
<ImplicitUsings>enable</ImplicitUsings>
<!-- HellionChat fork: assembly is renamed so Dalamud uses
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo,
keeping our state independent from the upstream plugin.
Code namespace stays ChatTwo.* so upstream cherry-picks
apply cleanly. -->
<Nullable>enable</Nullable>
<!-- Honor packages.lock.json on restore so floating version ranges
don't silently drift between machines or CI runs. -->
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<!-- v1.0.0 standalone cut — both AssemblyName and RootNamespace
are HellionChat. The plugin no longer maintains source-level
cherry-pick compatibility with upstream Infiziert90/ChatTwo;
upstream changes are integrated manually if at all. -->
<AssemblyName>HellionChat</AssemblyName>
<RootNamespace>ChatTwo</RootNamespace>
<RootNamespace>HellionChat</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" />
<!-- Closed ranges on packages with breaking-change history block a
surprise major bump when the lock file is regenerated. The
lock file pins the exact version per build; the upper bound
keeps the unlock path from drifting across major lines. -->
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
<!-- Override the transitively-referenced native SQLite build to one
that ships SQLite >= 3.50.3 (CVE-2025-6965 memory corruption,
CVE-2025-7709 fixed in 3.50.x). Microsoft.Data.Sqlite 10.0.7
pulls SQLitePCLRaw 2.1.11 which carries the older lib; pinning
the lib package directly forces the newer native binary
without a major bump on the managed wrapper. -->
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
<PackageReference Include="morelinq" Version="4.4.0" />
<PackageReference Include="Pidgin" Version="3.3.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
<PackageReference Include="Pidgin" Version="[3.5.1, 4.0.0)" />
<PackageReference Include="SixLabors.ImageSharp" Version="[3.1.12, 4.0.0)" />
</ItemGroup>
<ItemGroup>
<!-- Pure-function test suites in HellionChat.Tests need access to
the internal helper classes (StringUtil, UriPayload, Tokenizer
etc.). Test assembly does not get redistributed. -->
<InternalsVisibleTo Include="HellionChat.Tests" />
</ItemGroup>
<ItemGroup>
@@ -54,6 +75,9 @@
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
<LogicalName>HellionFont-OFL.txt</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Themes\Builtin\example-theme.json">
<LogicalName>HellionChat.Themes.Builtin.example-theme.json</LogicalName>
</EmbeddedResource>
</ItemGroup>
+301
View File
@@ -0,0 +1,301 @@
name: Hellion Chat
author: JonKazama-Hellion
punchline: Chat replacement with privacy controls aligned to EU, US and JP rules — based on Chat 2 (EUPL-1.2)
description: |-
Hellion Chat is a privacy-focused chat replacement for FINAL FANTASY XIV
based on the Chat 2 codebase (EUPL-1.2). One feature is intentionally
removed (the optional webinterface) and a stack of privacy controls is
added on top. Tabs, channel filters, RGB colours, emotes, screenshot
mode, IPC integration and the chat replacement window itself work the
same. The webinterface is intentionally not part of Hellion Chat because
it serves a different use case from the smaller default footprint this
plugin is built around.
On top of that, Hellion Chat adds privacy and data-handling controls
designed to align with the modern data protection rules that apply
across the EU, the United States and Japan. By default only your own
conversations are stored; messages from strangers, NPCs and system
spam stay out of the database. Retention windows are configurable per
channel, history can be wiped retroactively, and stored data can be
exported on demand.
Key privacy and data-handling features:
- Channel whitelist with a Privacy-First default
- Per-channel retention with a daily background sweep
- Retroactive cleanup with a Ctrl+Shift confirm
- Export to Markdown, JSON or CSV
- First-run wizard with three preset profiles (Privacy-First, Casual,
Full History)
- Bilingual UI (English and German) with live language switching
- Independent plugin state — own config file and database directory,
so Hellion Chat does not share state with upstream Chat 2
v1.1.0 — Theme engine with five built-in themes (Hellion Arctic,
Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove) plus
JSON-based custom-theme authoring. Settings rebuilt around a card
grid with section detail views. See docs/THEME-AUTHORING.md.
Based on Chat 2 by Infi and Anna, licensed under EUPL-1.2.
Modding & support: join the Hellion Forge Discord at
https://discord.gg/X9V7Kcv5gR — community for Hellion Chat and
other Hellion Online Media plugins/tools.
repo_url: https://github.com/JonKazama-Hellion/HellionChat
accepts_feedback: true
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png
image_urls:
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/chatWindow.png
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/settingsOverview.png
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/themesPicker.png
tags:
- Social
- UI
- Chat
- Replacement
- Privacy
changelog: |-
**Hellion Chat 1.1.0 — Theme Foundation**
First major UI cycle after the standalone v1.0.0 cut. Theme engine,
five built-in themes, customisable JSON themes, modernised settings
layout.
New themes (Settings → Themes):
- **Hellion Arctic** — the brand default, Arctic Cyan + Ember Glow
on industrial slate.
- **Chat 2 Klassik** — Steel Blue on neutral grey, eckige Kanten.
The upstream Chat 2 look on the new engine.
- **Event Horizon** — Cosmic Purple on near-black. Deep-space mood.
- **Moonlit Bloom** — Bloom Magenta + Soft Sage on deep-violet
night.
- **Mint Grove** — Mint Green + Honey Amber on deep forest. First
member of the Grove family.
Theme engine highlights:
- Slug-based selection in Settings → Themes with mini-mockup
previews per theme.
- Click a theme card and the whole plugin (chat, settings,
pop-outs, viewer) repaints instantly.
- Custom themes via JSON in pluginConfigs/HellionChat/themes/.
Example template seeded on first launch.
- Optional per-theme chat-channel colours. When a theme proposes
its own chat colours and yours differ, a dezent banner offers
Apply / Keep — never auto-overwriting.
- Migration v13 → v14: existing users land on Hellion Arctic. Pick
Chat 2 Klassik to keep the upstream look.
Settings layout:
- New card-grid overview on Settings open. Click a card to drill
into the section.
- Breadcrumb back to overview, ESC also returns.
- Detail view drops the redundant tab list — section content uses
the full width.
Branding:
- Plugin icon swapped from the ChatTwo derivative to the Hellion
Forge hammer.
- New docs/THEME-AUTHORING.md walks you through writing your own
themes with the Forge logo on top.
Technical:
- HellionStyle.PushGlobal is now theme-driven. Configuration.
HellionThemeEnabled is deprecated and will be removed in v1.2.0.
- New ThemeRegistry singleton with LastWriteTime-cached custom-
theme loader.
- 51 local unit tests cover the data model, registry, JSON round-
trip and built-in sanity checks.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 1.0.3 — Polish patch**
- New: optionally hide chat (and every other plugin window) while the
New Game+ menu is open. Toggle in Settings → Window → Frame, default
off. Closing the menu restores all windows.
- New: optionally tint the channel selector button next to the input
field with the currently active channel's colour. Toggle in
Settings → Appearance → Colours, default on. Matches the existing
input-text tint and respects ExtraChat overrides.
- Fix: status, item and other inline hover icons keep their original
aspect ratio. Debuff icons with non-square dimensions are no longer
visually squished into a 32×32 box.
- Diagnostic: hide-state transitions (battle, cutscene, user-hide,
cutscene override) are now logged on Verbose level for easier bug
reports — off by default, enable with `/xllog set HellionChat verbose`.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 1.0.1 — Window Position Recovery**
- Automatic bounds check on the first draw after plugin load.
When the persisted window position has no overlap with the
primary viewport, the window snaps to a safe top-left default.
Helpful after a monitor disconnect, resolution change or
multi-monitor layout switch between sessions.
- New "Reset Window Position" button in Settings → Window → Frame
as a manual escape hatch for edge cases the automatic check
doesn't catch.
Tested on Linux/Wayland with a hard-cut three-monitor reduction;
window recovers cleanly without manual JSON editing.
Housekeeping carried over since v1.0.0:
- Documentation restructured into docs/ folder. New CHANGELOG,
CONTRIBUTORS, LEARNING-JOURNEY and ROADMAP added
- Stale ChatTwo/* paths in repo configs updated to HellionChat/*
- Pidgin parser library bumped from 3.3.0 to 3.5.1 (CIString
Unicode fix relevant for non-ASCII channel/tab names)
- GitHub Actions: actions/setup-dotnet bumped 4 → 5,
github/codeql-action bumped 3 → 4
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 1.0.0 — Standalone Major Release**
First fully standalone release. Internal cleanup plus a sweep of
pre-existing correctness, security, threading and resource-leak
fixes carried over from the upstream codebase. No user action
required — auto-update applies cleanly, configuration and database
paths unchanged.
Standalone identity:
- Code namespace consolidated from ChatTwo.* to HellionChat.* across
all source files
- IPC channels migrated from ChatTwo.* to HellionChat.* (6 channels:
Register, Available, Unregister, Invoke, GetChatInputState,
ChatInputStateChanged) — third-party plugins that bound to the old
channels need to be updated; none known at release time
- ImGui popup ID renamed to hellionchat-context-popup
- Repository folder restructured (ChatTwo/ → HellionChat/), all CI
and build paths updated accordingly
- Public-facing descriptions reworded from upstream-fork framing to
standalone framing (Chat 2 attribution preserved per EUPL-1.2)
- Colour preset 'ChatTwo Default' is now 'Klassik (Chat 2 Default)'
Safety:
- Plugin now refuses to load when upstream Chat 2 is also active —
bilingual conflict message in EN/DE, throw before any subsystem
initialization, prevents the runtime crash that previously occurred
when both plugins replaced the same chat window in parallel
- SQLite native binary bumped to 3.50.3 (CVE-2025-6965 memory
corruption from aggregate-term overflow, CVE-2025-7709)
- NuGet restore now honors packages.lock.json so transitive
dependencies don't drift between machines or CI runs
Default tab layout sharpened (one-time tab reset on first start):
The first-run tab layout is reorganized into five thematic tabs
based on external tester feedback. General contains only Say,
Yell and Shout (immediate-surroundings public chat). System
absorbs the gameplay-event streams (NpcDialogue, Loot, Crafting,
Gathering, PF recruitment pings) and announcement noise
(BattleSystem, FreeCompanyAnnouncement, PvpTeamAnnouncement)
that previously lived in General. FreeCompany, Group and
Linkshell each own their channel set. The static Tell tab is
gone — Auto-Tell-Tabs spawns per-conversation tabs on demand.
The Beginner / Novice-Network preset is no longer added by
default but is still available via Settings, Tabs.
This is a one-time tab-layout reset for users on config version
12 or older. Privacy, Retention, Theme and every other setting
is preserved. Your previous tab configuration is written to
pluginConfigs/HellionChat.json.pre-v13-backup so you can restore
it manually if you prefer the old layout.
Crash-class fixes (formerly latent in upstream):
- MathUtil.HasOverlap now uses a correct AABB test; identical or
edge-touching rectangles are no longer reported as non-overlapping
- ChatCode.Equals compares fields directly instead of GetHashCode;
removes the hash-collision anti-pattern
- IpcManager.Dispose uses UnregisterAction to match the matching
RegisterAction call; previous mismatch leaked the action
subscription on every plugin reload
- ExtraChat.Dispose now unsubscribes all three IPC subscriptions
(was only the first); leaks closed
- TellTarget.FromTarget guards against a zero IPlayerCharacter.Address
before dereferencing the unsafe Character* cast
- GameFunctions ResolveTextCommandPlaceholderDetour null-checks the
Hook reference instead of using the null-forgiving operator
- Popout.cs and SettingsTabs/Tabs.cs bounds-check list indexing so
a tab drop or empty-worlds list no longer crashes the UI
- Debugger.cs now declares IDisposable so the existing Dispose runs
Correctness fixes:
- GlobalParametersCache.GetValue captures Cache into a local before
the bounds check, so a concurrent Refresh can't slip a different
array between check and read
- IconUtil binary search bounds initialized to entries.Length-1 and
reset on redirect-restart; entries.Length==0 short-circuits
- Sheets.WorldsOnDatacenter now compares DataCenter.RowId (was
Region.RowId) so it actually returns same-DC worlds
- Message.cs back-reference loop iterates the processed Sender/Content
properties so chunks added by CheckMessageContent get Message set
- Language.zh-Hans Webinterface_Start_Success corrected to
"网页界面已启动" (was "网页界面已停止")
Threading and async:
- AutoTranslate Entries/ValidEntries are now serialized behind a
single lock; the preload worker thread and main thread no longer
race on the underlying dictionary/hash set
- Privacy retention and cleanup workers bound their framework-refresh
waits to 5 seconds with a logged timeout; a hung framework tick can
no longer deadlock the background worker
Resource handling:
- EmoteCache reuses the static HttpClient instead of allocating a new
one per call (closed socket leak)
- FontManager wraps HttpClient/HttpResponseMessage in using-blocks
and adds EnsureSuccessStatusCode; failed downloads no longer
silently produce a zero-byte font file
- SearchSelector mixes the row index into the ImGui ID stack so
selectables don't collapse to a single ambiguous ID
- SettingsTabs/Chat blocked-emote add-button now opens its selector
popup on left-click
Performance:
- DbViewer text export caches filteredHistory.Count once instead of
re-enumerating the IEnumerable on every batch (O(N) instead of
O(N²) on large histories)
License attribution (NOTICE.md, COPYRIGHT, THIRD_PARTY_NOTICES.md
and the Credits section in README) is unchanged.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.6.1 — Pop-Out Discoverability & /tell Auto-Pop-Out**
- Pop-out button now visible in the chat header (no more hunting
through the right-click menu)
- One-time hint banner explains pop-out tabs and the right-click
shortcut
- New setting: open new /tell tabs directly as pop-out windows
(Settings → Chat → Auto-Tell-Tabs)
- Pop-out input is now enabled by default — closing a pop-out still
returns the tab to the sidebar
- Bugfix: dropping or logging out with an LRU/popped auto-tell tab
now also closes its pop-out window (no more ghost windows)
- Bugfix: dead zone below the chat input bar when the v0.6.0 pop-out
hint banner was visible (also fixed retroactively for the v0.6.0
banner inside pop-outs)
Modding & support: join Hellion Forge — https://discord.gg/X9V7Kcv5gR
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
Earlier history: https://github.com/JonKazama-Hellion/HellionChat/releases
@@ -1,6 +1,6 @@
using System.Collections.Generic;
namespace ChatTwo;
namespace HellionChat;
// Hellion Chat — v0.6.0 shared input history. Replaces the embedded
// ChatLogWindow.InputBacklog so that pop-out windows with their own
@@ -1,6 +1,6 @@
using Dalamud.Plugin.Ipc;
namespace ChatTwo.Ipc;
namespace HellionChat.Ipc;
public sealed class ExtraChat : IDisposable
{
@@ -20,10 +20,14 @@ public sealed class ExtraChat : IDisposable
internal (string, uint)? ChannelOverride { get; set; }
private Dictionary<string, uint> ChannelCommandColoursInternal { get; set; } = new();
// Volatile reference: IPC callbacks (OnChannelCommandColours/OnChannelNames) fire on a
// Dalamud-dispatcher thread while the ImGui thread reads the IReadOnlyDictionary projections.
// Reference assignment is atomic on x64, but the JIT (especially Mono on Wine/Linux) needs
// the volatile barrier to guarantee visibility across threads. See AUDIT-2026-05-05 [SEC-01].
private volatile Dictionary<string, uint> ChannelCommandColoursInternal = new();
internal IReadOnlyDictionary<string, uint> ChannelCommandColours => ChannelCommandColoursInternal;
private Dictionary<Guid, string> ChannelNamesInternal { get; set; } = new();
private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
internal ExtraChat()
@@ -40,15 +44,18 @@ public sealed class ExtraChat : IDisposable
ChannelCommandColoursInternal = ChannelCommandColoursGate.InvokeFunc(null!);
ChannelNamesInternal = ChannelNamesGate.InvokeFunc(null!);
}
catch (Exception)
catch (Exception ex)
{
// no-op
// ExtraChat is optional; missing IPC peer is normal when the plugin isn't loaded.
Plugin.Log.Verbose(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
}
}
public void Dispose()
{
OverrideChannelGate.Unsubscribe(OnOverrideChannel);
ChannelCommandColoursGate.Unsubscribe(OnChannelCommandColours);
ChannelNamesGate.Unsubscribe(OnChannelNames);
}
private void OnOverrideChannel(OverrideInfo info)
@@ -1,7 +1,7 @@
using ChatTwo.Code;
using HellionChat.Code;
using Dalamud.Plugin.Ipc;
namespace ChatTwo.Ipc;
namespace HellionChat.Ipc;
using ChatInputState = (bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType);
@@ -19,8 +19,8 @@ internal sealed class TypingIpc : IDisposable
{
Plugin = plugin;
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>("ChatTwo.GetChatInputState");
StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>("ChatTwo.ChatInputStateChanged");
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>("HellionChat.GetChatInputState");
StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>("HellionChat.ChatInputStateChanged");
StateQueryGate.RegisterFunc(GetState);
}
@@ -2,7 +2,7 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Ipc;
namespace ChatTwo;
namespace HellionChat;
internal sealed class IpcManager : IDisposable
{
@@ -15,15 +15,15 @@ internal sealed class IpcManager : IDisposable
public IpcManager()
{
RegisterGate = Plugin.Interface.GetIpcProvider<string>("ChatTwo.Register");
RegisterGate = Plugin.Interface.GetIpcProvider<string>("HellionChat.Register");
RegisterGate.RegisterFunc(Register);
AvailableGate = Plugin.Interface.GetIpcProvider<object?>("ChatTwo.Available");
AvailableGate = Plugin.Interface.GetIpcProvider<object?>("HellionChat.Available");
UnregisterGate = Plugin.Interface.GetIpcProvider<string, object?>("ChatTwo.Unregister");
UnregisterGate = Plugin.Interface.GetIpcProvider<string, object?>("HellionChat.Unregister");
UnregisterGate.RegisterAction(Unregister);
InvokeGate = Plugin.Interface.GetIpcProvider<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?>("ChatTwo.Invoke");
InvokeGate = Plugin.Interface.GetIpcProvider<string, PlayerPayload?, ulong, Payload?, SeString?, SeString?, object?>("HellionChat.Invoke");
AvailableGate.SendMessage();
}
@@ -47,7 +47,7 @@ internal sealed class IpcManager : IDisposable
public void Dispose()
{
UnregisterGate.UnregisterFunc();
UnregisterGate.UnregisterAction();
RegisterGate.UnregisterFunc();
Registered.Clear();
}
@@ -1,6 +1,6 @@
using System.Text;
using ChatTwo.Code;
using ChatTwo.Util;
using HellionChat.Code;
using HellionChat.Util;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using System.Text.RegularExpressions;
@@ -8,7 +8,7 @@ using Dalamud.Game.Text;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace ChatTwo;
namespace HellionChat;
public partial class Message
{
@@ -49,7 +49,10 @@ public partial class Message
ExtraChatChannel = extraChatChannel;
Hash = GenerateHash();
foreach (var chunk in sender.Concat(content))
// Iterate the processed Content list (returned by CheckMessageContent)
// rather than the raw constructor parameter — chunks added or replaced
// by CheckMessageContent must also have their back-reference set.
foreach (var chunk in Sender.Concat(Content))
chunk.Message = this;
}
@@ -1,9 +1,9 @@
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using ChatTwo.Code;
using ChatTwo.Resources;
using ChatTwo.Util;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Util;
using Dalamud.Game.Chat;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
@@ -15,7 +15,7 @@ using Lumina.Text.Expressions;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;
namespace ChatTwo;
namespace HellionChat;
internal class MessageManager : IAsyncDisposable
{
@@ -34,7 +34,10 @@ internal class MessageManager : IAsyncDisposable
// After that, the message is enqueued in the PendingAsync queue, which will
// be consumed in a separate thread and perform more processing (emotes,
// URLs) as well as inserting the message into the database.
private Queue<PendingMessage> PendingSync { get; } = [];
// LinkedList instead of Queue: ContentIdResolver hits PendingSync.Last
// every hook call. Queue<T>.Last() is the LINQ extension and walks the
// whole queue (O(n)); LinkedList<T>.Last is an O(1) node reference.
private LinkedList<PendingMessage> PendingSync { get; } = [];
private ConcurrentQueue<PendingMessage> PendingAsync { get; } = [];
private readonly Thread PendingMessageThread;
private readonly CancellationTokenSource PendingThreadCancellationToken = new();
@@ -93,6 +96,10 @@ internal class MessageManager : IAsyncDisposable
Plugin.Log.Debug("Sleeping because PendingMessageThread thread still alive");
}
// CancellationTokenSource owns an unmanaged WaitHandle; dispose after the
// worker thread has drained, otherwise it leaks across plugin reloads.
PendingThreadCancellationToken.Dispose();
Store.Dispose();
}
@@ -113,8 +120,11 @@ internal class MessageManager : IAsyncDisposable
LastContentId = contentId;
// Drain the PendingSync queue into the PendingAsync queue.
while (PendingSync.TryDequeue(out var pending))
PendingAsync.Enqueue(pending);
while (PendingSync.First is { } first)
{
PendingSync.RemoveFirst();
PendingAsync.Enqueue(first.Value);
}
}
private void ProcessPendingMessages(CancellationToken token)
@@ -219,7 +229,7 @@ internal class MessageManager : IAsyncDisposable
// We delay messages to be handed off to the async processing thread
// in the next tick, otherwise we can't get the content ID from the hook
// below.
PendingSync.Enqueue(pendingMessage);
PendingSync.AddLast(pendingMessage);
}
// This hook is called immediately after receiving a message with the
@@ -231,11 +241,11 @@ internal class MessageManager : IAsyncDisposable
try
{
ContentIdResolverHook?.Original(agent, contentId, accountId, messageIndex, worldId, chatType);
if (PendingSync.Count == 0)
if (PendingSync.Last is not { } last)
return;
PendingSync.Last().ContentId = contentId;
PendingSync.Last().AccountId = accountId;
last.Value.ContentId = contentId;
last.Value.AccountId = accountId;
}
catch (Exception ex)
{
@@ -1,9 +1,9 @@
using System.Buffers;
using System.Collections;
using System.Data.Common;
using ChatTwo.Code;
using ChatTwo.Ui;
using ChatTwo.Util;
using HellionChat.Code;
using HellionChat.Ui;
using HellionChat.Util;
using Dalamud.Game.Text.SeStringHandling;
using MessagePack;
using MessagePack.Formatters;
@@ -13,7 +13,7 @@ using Microsoft.Data.Sqlite;
using DalamudUtil = Dalamud.Utility.Util;
using Encoding = System.Text.Encoding;
namespace ChatTwo;
namespace HellionChat;
internal static class DbExtensions
{
@@ -452,7 +452,10 @@ internal class MessageStore : IDisposable
// covers any future write paths e.g. webinterface backfill).
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
{
Plugin.Log.Debug($"Privacy filter dropped message: ChatType={message.Code.Type}");
// Verbose-only: this fires for every dropped message, which is
// the common case for users with a tight privacy whitelist. Keep
// it for diagnostics but stay out of the default xllog stream.
Plugin.Log.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}");
return;
}
@@ -1,8 +1,8 @@
using System.Numerics;
using ChatTwo.Code;
using ChatTwo.Resources;
using ChatTwo.Ui;
using ChatTwo.Util;
using HellionChat.Code;
using HellionChat.Resources;
using HellionChat.Ui;
using HellionChat.Util;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects.SubKinds;
@@ -22,13 +22,13 @@ using Dalamud.Bindings.ImGui;
using Lumina.Excel.Sheets;
using Action = System.Action;
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
using ChatTwoPartyFinderPayload = ChatTwo.Util.PartyFinderPayload;
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
namespace ChatTwo;
namespace HellionChat;
public sealed class PayloadHandler
{
private const string PopupId = "chat2-context-popup";
private const string PopupId = "hellionchat-context-popup";
private ChatLogWindow LogWindow { get; }
private (Chunk, Payload?)? Popup { get; set; }
@@ -332,10 +332,19 @@ public sealed class PayloadHandler
atkBase->SetPosition((short) x, (short) y);
}
private const float MaxInlineIconSize = 32f;
private static void InlineIcon(IDalamudTextureWrap icon)
{
if (icon.Size.X <= 0 || icon.Size.Y <= 0)
return;
var width = (float) icon.Size.X;
var height = (float) icon.Size.Y;
var scale = Math.Min(1f, Math.Min(MaxInlineIconSize / width, MaxInlineIconSize / height));
var size = ImGuiHelpers.ScaledVector2(width * scale, height * scale);
var cursor = ImGui.GetCursorPos();
var size = ImGuiHelpers.ScaledVector2(32, 32);
ImGui.Image(icon.Handle, size);
ImGui.SameLine();
ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing()));
+148 -22
View File
@@ -2,10 +2,10 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO;
using ChatTwo.Ipc;
using ChatTwo.Resources;
using ChatTwo.Ui;
using ChatTwo.Util;
using HellionChat.Ipc;
using HellionChat.Resources;
using HellionChat.Ui;
using HellionChat.Util;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Windowing;
using Dalamud.IoC;
@@ -14,7 +14,7 @@ using Dalamud.Plugin.Services;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiFileDialog;
namespace ChatTwo;
namespace HellionChat;
// ReSharper disable once ClassNeverInstantiated.Global
public sealed class Plugin : IDalamudPlugin
@@ -63,6 +63,7 @@ public sealed class Plugin : IDalamudPlugin
internal ExtraChat ExtraChat { get; }
internal TypingIpc TypingIpc { get; }
internal FontManager FontManager { get; }
internal Themes.ThemeRegistry ThemeRegistry { get; private set; } = null!;
internal int DeferredSaveFrames = -1;
@@ -94,6 +95,12 @@ public sealed class Plugin : IDalamudPlugin
public Plugin()
{
// Refuse to start if upstream Chat 2 is loaded — prevents IPC
// channel collisions and double-replacement of the in-game chat
// window. Throwing here makes Dalamud abort the load cleanly with
// our localized message instead of crashing FFXIV mid-frame.
ChatTwoConflictDetector.ThrowIfChatTwoIsLoaded(Interface);
try
{
GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime();
@@ -184,22 +191,89 @@ public sealed class Plugin : IDalamudPlugin
"SeenPopOutHeaderHint reset to false (v0.6.1 banner re-armed)");
}
// Hellion default tab layout for first-run and v10-wipe.
// General catches player chat plus active gameplay events; the
// System tab takes the technical noise so it does not bury real
// conversation. Beginner tab only appears when the Novice
// Network is enabled in Audio and Notifications, otherwise it
// would just sit empty.
// Hellion Chat v12 → v13 — hard-resets the tab layout to the
// sharpened v1.0.0 defaults (5 thematic tabs, see TabsUtil and
// the default-fill block below). Existing tab state is wiped
// because per-channel mapping from the old General preset to
// the new General/System split would be ambiguous and would
// produce subtly wrong results for users who tweaked the old
// layout. A timestamped backup of the live config is written
// alongside it as a manual restore safety net. The wipe scope
// is intentionally narrow: only Config.Tabs is reset; Privacy,
// Retention, Theme and every other knob keeps its current value.
if (Config.Version < 13)
{
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
if (pluginConfigsDir is not null)
{
var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v13-backup");
try
{
if (File.Exists(liveConfigPath))
File.Copy(liveConfigPath, backupPath, overwrite: true);
}
catch (Exception ex)
{
Log.Warning(ex, "HellionChat: pre-v13 config backup failed");
}
}
Config.Tabs.Clear();
Config.Version = 13;
SaveConfig();
Log.Information(
"Migrated config v12 → v13: tab layout hard-reset to v1.0.0 defaults; " +
"pre-v13 config backup written next to the live file. " +
"Default tabs will be populated by the Tabs.Count == 0 block.");
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
{
Title = HellionStrings.SettingsRefactor_Migration_Title,
Content = HellionStrings.SettingsRefactor_Migration_Content,
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
InitialDuration = TimeSpan.FromSeconds(25),
});
}
// Hellion Chat v13 → v14 — theme-engine migration. Alle User landen
// auf "hellion-arctic" als neues Default-Theme; die alte
// HellionThemeEnabled-Flag wird deprecated und nur noch ein Release
// als Safety-Net im JSON behalten. Window-Opacity wandert von
// HellionThemeWindowOpacity in das neue WindowOpacity-Feld.
if (Config.Version < 14)
{
Config.Theme = "hellion-arctic";
#pragma warning disable CS0612, CS0618 // Obsolete: HellionThemeWindowOpacity bleibt readable bis v1.2.0
Config.WindowOpacity = Config.HellionThemeWindowOpacity;
#pragma warning restore CS0612, CS0618
Config.ReduceMotion = false;
Config.UseCompactDensity = false;
Config.ShowThemeQuickPicker = false;
Config.Version = 14;
SaveConfig();
Log.Information(
"Migrated config v13 → v14: theme engine introduced, all users land on hellion-arctic; " +
"pick chat2-classic in Settings → Themes for the upstream look");
}
// Hellion v1.0.0 default tab layout. Five thematically separated
// tabs: General catches the immediate-surroundings public chat
// (Say/Yell/Shout) only; System absorbs the rest of the technical
// and gameplay-event noise; FreeCompany, Group and Linkshell each
// own their respective channel set. Tells are not in a static
// tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation
// tabs on demand. Novice-Network gets no preset tab; users who
// want it can add HellionBeginner from Settings → Tabs.
if (Config.Tabs.Count == 0)
{
Config.Tabs.Add(TabsUtil.VanillaGeneral);
Config.Tabs.Add(TabsUtil.HellionSystem);
Config.Tabs.Add(TabsUtil.HellionFreeCompany);
Config.Tabs.Add(TabsUtil.HellionParty);
if (Config.ShowNoviceNetwork)
Config.Tabs.Add(TabsUtil.HellionBeginner);
Config.Tabs.Add(TabsUtil.HellionLinkshell);
Config.Tabs.Add(TabsUtil.VanillaTellExclusive);
}
LanguageChanged(Interface.UiLanguage);
@@ -214,6 +288,14 @@ public sealed class Plugin : IDalamudPlugin
ExtraChat = new ExtraChat();
FontManager = new FontManager();
// v1.1.0 — Theme-Engine init. Custom-Themes liegen in
// pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get.
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
Directory.CreateDirectory(customThemesDir);
SeedExampleThemeIfEmpty(customThemesDir);
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
ThemeRegistry.Switch(Config.Theme);
MessageManager = new MessageManager(this); // Does it require UI?
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
@@ -477,10 +559,15 @@ public sealed class Plugin : IDalamudPlugin
if (deleted > 0)
{
Log.Information($"Retention sweep deleted {deleted} expired messages.");
// Run the clear+refilter synchronously on the framework thread.
// Earlier this called FilterAllTabsAsync(), which is fire-and-forget
// — the .Wait() here would return as soon as the inner Task.Run was
// dispatched, racing the next sweep cycle against the still-running
// filter pass. See AUDIT-2026-05-05 [QUAL-02].
Framework.Run(() =>
{
MessageManager.ClearAllTabs();
MessageManager.FilterAllTabsAsync();
MessageManager.FilterAllTabs();
}).Wait();
}
else
@@ -502,13 +589,10 @@ public sealed class Plugin : IDalamudPlugin
private void Draw()
{
// Hellion theme is pushed once per frame here so every plugin window
// (chat log, settings, viewers, wizard, file dialog) renders with
// the same palette. Skipping the push leaves the upstream Dalamud
// look untouched for users who flipped the toggle off.
using IDisposable? _style = Config.HellionThemeEnabled
? HellionStyle.PushGlobal(Config.HellionThemeWindowOpacity)
: null;
// Theme-Engine ist ab v14 immer aktiv; Klassik ist jetzt ein eigenes
// Theme statt einem deaktivierten Hellion-Theme. Active wird einmal
// pro Frame aus der Registry gelesen.
using IDisposable _style = HellionStyle.PushGlobal(ThemeRegistry.Active, Config.WindowOpacity);
ChatLogWindow.BeginFrame();
@@ -519,6 +603,16 @@ public sealed class Plugin : IDalamudPlugin
return;
}
// v1.0.2 — global skip while the New Game+ menu (QuestRedo addon) is
// open. Hides every plugin window in one shot (chat log, pop-outs,
// settings, db viewer, etc.), matching the LoadingScreens pattern.
if (Config.HideInNewGamePlusMenu && GameFunctions.GameFunctions.IsAddonInteractable(GameFunctions.GameFunctions.NewGamePlusAddonName))
{
ChatLogWindow.FinalizeFrame();
TypingIpc.Update();
return;
}
ChatLogWindow.HideStateCheck();
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
@@ -583,4 +677,36 @@ public sealed class Plugin : IDalamudPlugin
public static bool InBattle => Condition[ConditionFlag.InCombat];
public static bool GposeActive => Condition[ConditionFlag.WatchingCutscene];
public static bool CutsceneActive => Condition[ConditionFlag.OccupiedInCutSceneEvent] || Condition[ConditionFlag.WatchingCutscene78];
// v1.1.0 — wenn der themes/-Ordner leer ist, schreiben wir die embedded
// example-theme.json als Vorlage rein. Bestehende User-Customs werden
// nicht angefasst (existing JSONs lassen den Block überspringen).
private static void SeedExampleThemeIfEmpty(string dir)
{
if (Directory.EnumerateFiles(dir, "*.json").Any())
return;
var examplePath = Path.Combine(dir, "example-theme.json");
var resourceStream = typeof(Plugin).Assembly.GetManifestResourceStream("HellionChat.Themes.Builtin.example-theme.json");
if (resourceStream is null)
{
Log.Warning("Themes example template not found in assembly resources; skipping seed.");
return;
}
try
{
using var fileStream = File.Create(examplePath);
resourceStream.CopyTo(fileStream);
Log.Information($"Seeded example-theme.json into {dir}");
}
catch (IOException ex)
{
Log.Warning(ex, "Failed to seed example-theme.json; user can create custom themes manually.");
}
finally
{
resourceStream.Dispose();
}
}
}
@@ -1,6 +1,6 @@
using ChatTwo.Code;
using HellionChat.Code;
namespace ChatTwo.Privacy;
namespace HellionChat.Privacy;
internal static class PrivacyDefaults
{
@@ -1,8 +1,8 @@
using System.Collections.Generic;
using ChatTwo.Code;
using ChatTwo.Util;
using HellionChat.Code;
using HellionChat.Util;
namespace ChatTwo.Resources;
namespace HellionChat.Resources;
// Hellion Chat — v0.6.0 built-in colour presets for the ChatColours
// settings section. Read-only static data; users apply a preset via the
@@ -8,7 +8,7 @@
#nullable enable
namespace ChatTwo.Resources;
namespace HellionChat.Resources;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
@@ -26,7 +26,7 @@ internal class HellionStrings
get
{
if (resourceMan is null)
resourceMan = new global::System.Resources.ResourceManager("ChatTwo.Resources.HellionStrings", typeof(HellionStrings).Assembly);
resourceMan = new global::System.Resources.ResourceManager("HellionChat.Resources.HellionStrings", typeof(HellionStrings).Assembly);
return resourceMan;
}
}
@@ -203,6 +203,37 @@ internal class HellionStrings
internal static string Settings_Tab_Database => Get(nameof(Settings_Tab_Database));
internal static string Settings_Tab_Information => Get(nameof(Settings_Tab_Information));
// v1.1.0 — Settings card-grid overview
internal static string Settings_Card_General_Title => Get(nameof(Settings_Card_General_Title));
internal static string Settings_Card_General_Subtext => Get(nameof(Settings_Card_General_Subtext));
internal static string Settings_Card_Appearance_Title => Get(nameof(Settings_Card_Appearance_Title));
internal static string Settings_Card_Appearance_Subtext => Get(nameof(Settings_Card_Appearance_Subtext));
internal static string Settings_Card_Themes_Title => Get(nameof(Settings_Card_Themes_Title));
internal static string Settings_Card_Themes_Subtext => Get(nameof(Settings_Card_Themes_Subtext));
internal static string Settings_Card_Window_Title => Get(nameof(Settings_Card_Window_Title));
internal static string Settings_Card_Window_Subtext => Get(nameof(Settings_Card_Window_Subtext));
internal static string Settings_Card_Chat_Title => Get(nameof(Settings_Card_Chat_Title));
internal static string Settings_Card_Chat_Subtext => Get(nameof(Settings_Card_Chat_Subtext));
internal static string Settings_Card_Tabs_Title => Get(nameof(Settings_Card_Tabs_Title));
internal static string Settings_Card_Tabs_Subtext => Get(nameof(Settings_Card_Tabs_Subtext));
internal static string Settings_Card_Privacy_Title => Get(nameof(Settings_Card_Privacy_Title));
internal static string Settings_Card_Privacy_Subtext => Get(nameof(Settings_Card_Privacy_Subtext));
internal static string Settings_Card_Database_Title => Get(nameof(Settings_Card_Database_Title));
internal static string Settings_Card_Database_Subtext => Get(nameof(Settings_Card_Database_Subtext));
internal static string Settings_Card_Information_Title => Get(nameof(Settings_Card_Information_Title));
internal static string Settings_Card_Information_Subtext => Get(nameof(Settings_Card_Information_Subtext));
// v1.1.0 — Themes-Settings-Tab
internal static string Settings_Tab_Themes => Get(nameof(Settings_Tab_Themes));
internal static string Settings_Themes_Active => Get(nameof(Settings_Themes_Active));
internal static string Settings_Themes_BuiltIns => Get(nameof(Settings_Themes_BuiltIns));
internal static string Settings_Themes_Custom => Get(nameof(Settings_Themes_Custom));
internal static string Settings_Themes_OpenFolder => Get(nameof(Settings_Themes_OpenFolder));
internal static string Settings_Themes_ExportActive => Get(nameof(Settings_Themes_ExportActive));
internal static string Settings_Themes_ApplyChatColors_Hint => Get(nameof(Settings_Themes_ApplyChatColors_Hint));
internal static string Settings_Themes_ApplyChatColors_Apply => Get(nameof(Settings_Themes_ApplyChatColors_Apply));
internal static string Settings_Themes_ApplyChatColors_Keep => Get(nameof(Settings_Themes_ApplyChatColors_Keep));
// Hellion Chat — General-Tab section headings
internal static string Settings_General_Input_Heading => Get(nameof(Settings_General_Input_Heading));
internal static string Settings_General_Audio_Heading => Get(nameof(Settings_General_Audio_Heading));
@@ -261,6 +292,10 @@ internal class HellionStrings
internal static string Settings_Window_PopOutInputEnabled_Name => Get(nameof(Settings_Window_PopOutInputEnabled_Name));
internal static string Settings_Window_PopOutInputEnabled_Description => Get(nameof(Settings_Window_PopOutInputEnabled_Description));
// Hellion Chat — Window position recovery (off-screen safety net)
internal static string Settings_Window_ResetPosition_Name => Get(nameof(Settings_Window_ResetPosition_Name));
internal static string Settings_Window_ResetPosition_Description => Get(nameof(Settings_Window_ResetPosition_Description));
// Hellion Chat — v0.6.0 one-time hint banner shown inside pop-outs
internal static string Popout_v060_HintText => Get(nameof(Popout_v060_HintText));
internal static string Popout_v060_HintAck => Get(nameof(Popout_v060_HintAck));
@@ -270,4 +305,9 @@ internal class HellionStrings
internal static string Hint_v061_PopOutHeader_Body => Get(nameof(Hint_v061_PopOutHeader_Body));
internal static string Hint_v061_PopOutHeader_Ack => Get(nameof(Hint_v061_PopOutHeader_Ack));
internal static string Hint_v061_PopOutHeader_OpenSettings => Get(nameof(Hint_v061_PopOutHeader_OpenSettings));
// Hellion Chat — v1.0.0 Chat 2 parallel-load conflict detection
internal static string ChatTwoConflictTitle => Get(nameof(ChatTwoConflictTitle));
internal static string ChatTwoConflictBody => Get(nameof(ChatTwoConflictBody));
internal static string ChatTwoConflictAction => Get(nameof(ChatTwoConflictAction));
}
@@ -19,7 +19,7 @@
<value>Datenschutz-Filter aktivieren</value>
</data>
<data name="Privacy_FilterEnabled_Description" xml:space="preserve">
<value>Wenn aktiviert, werden nur Nachrichten aus den erlaubten Kanälen in die Datenbank gespeichert. Beim Deaktivieren gilt wieder das Standard-Verhalten von ChatTwo, also alles außer Battle-Logs wird gespeichert.</value>
<value>Wenn aktiviert, werden nur Nachrichten aus den erlaubten Kanälen in die Datenbank gespeichert. Beim Deaktivieren gilt wieder das Standardverhalten, also alles außer Battle-Logs wird gespeichert.</value>
</data>
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
<value>Der Filter steuert nur, was in die lokale Datenbank geschrieben wird. Im Chat-Log siehst du weiterhin jede Nachricht live, ausgeschlossene Kanäle werden nur nicht mehr gespeichert. Wenn du Kanäle auch aus der sichtbaren Anzeige entfernen willst, nutze die normalen Chat-Tab-Filter im Spiel.</value>
@@ -211,7 +211,7 @@
<value>Volle Historie</value>
</data>
<data name="Wizard_Profile_FullHistory_Description" xml:space="preserve">
<value>Deaktiviert den Datenschutz-Filter komplett. Speichert alles außer Battle-Logs, wie das Original-Chat 2. Aufbewahrung ist AUS, die Historie wächst dauerhaft.</value>
<value>Deaktiviert den Datenschutz-Filter komplett. Speichert alles außer Battle-Logs (das ursprüngliche Voll-Historie-Verhalten). Aufbewahrung ist AUS, die Historie wächst dauerhaft.</value>
</data>
<data name="Wizard_Profile_FullHistory_GdprWarning" xml:space="preserve">
<value>DSGVO-Hinweis: Wenn du Nachrichten Dritter (Sagen/Schreien/Rufen fremder Spieler, NPC-Dialoge mit Spielernamen usw.) zeitlich unbegrenzt speicherst, kann das die Ausnahme für rein persönliche oder familiäre Tätigkeiten (Art. 2 Abs. 2 Buchst. c) sprengen. Nutze dieses Profil nur, wenn du einen klaren Grund hast, das volle Archiv zu behalten.</value>
@@ -562,7 +562,7 @@
<value>Wenn du mehrere Linkshells benutzt, empfiehlt der Maintainer einen Tab pro Shell für eine sauberere Übersicht. Tab duplizieren und je Kopie die Kanalauswahl einschränken.</value>
</data>
<data name="ChatColourPresets_Default" xml:space="preserve">
<value>ChatTwo Standard</value>
<value>Klassik (Chat 2 Default)</value>
</data>
<data name="ChatColourPresets_HighContrast" xml:space="preserve">
<value>Hoher Kontrast</value>
@@ -591,6 +591,12 @@
<data name="Settings_Window_PopOutInputEnabled_Description" xml:space="preserve">
<value>Master-Switch: erlaubt direktes Tippen und Absenden in jedem Pop-Out-Fenster (inkl. Auto-Tell-Tabs). Channel-Wechsel im Pop-Out wirkt global wie im Hauptfenster; Text-Buffer und History-Cursor sind pro Pop-Out unabhängig.</value>
</data>
<data name="Settings_Window_ResetPosition_Name" xml:space="preserve">
<value>Fenster-Position zurücksetzen</value>
</data>
<data name="Settings_Window_ResetPosition_Description" xml:space="preserve">
<value>Holt das Chat-Fenster und alle aktiven Pop-Outs zurück in die linke obere Ecke des Hauptmonitors. Hilfreich wenn ein Fenster nach einem Display-Layout-Wechsel außerhalb des sichtbaren Bereichs gelandet ist (Monitor abgezogen, Auflösung geändert). Das Plugin macht außerdem einmal pro Session einen automatischen Bounds-Check, dieser Button ist der manuelle Notausgang falls trotzdem etwas unerreichbar bleibt.</value>
</data>
<data name="Popout_v060_HintText" xml:space="preserve">
<value>Neu in v0.6.0: Du kannst jetzt direkt im Pop-Out tippen. Master-Switch in den Fenster-Settings aktivieren.</value>
</data>
@@ -609,4 +615,94 @@
<data name="Hint_v061_PopOutHeader_OpenSettings" xml:space="preserve">
<value>Einstellungen öffnen</value>
</data>
<data name="ChatTwoConflictTitle" xml:space="preserve">
<value>Hellion Chat kann nicht starten, solange Chat 2 geladen ist.</value>
</data>
<data name="ChatTwoConflictBody" xml:space="preserve">
<value>Hellion Chat ist ein eigenständiger Fork von Chat 2. Beide Plugins ersetzen dasselbe Chat-Fenster im Spiel und würden zur Laufzeit kollidieren.</value>
</data>
<data name="ChatTwoConflictAction" xml:space="preserve">
<value>Chat 2 in /xlplugins deaktivieren, danach Hellion Chat erneut aktivieren.</value>
</data>
<data name="Settings_Card_General_Title" xml:space="preserve">
<value>Allgemein</value>
</data>
<data name="Settings_Card_General_Subtext" xml:space="preserve">
<value>Sprache und grundlegendes Verhalten</value>
</data>
<data name="Settings_Card_Appearance_Title" xml:space="preserve">
<value>Erscheinungsbild</value>
</data>
<data name="Settings_Card_Appearance_Subtext" xml:space="preserve">
<value>Fensterdeckkraft, Schriften, Bewegung</value>
</data>
<data name="Settings_Card_Themes_Title" xml:space="preserve">
<value>Themes</value>
</data>
<data name="Settings_Card_Themes_Subtext" xml:space="preserve">
<value>Theme wählen oder eigenes importieren</value>
</data>
<data name="Settings_Card_Window_Title" xml:space="preserve">
<value>Fenster</value>
</data>
<data name="Settings_Card_Window_Subtext" xml:space="preserve">
<value>Fensterposition, Rahmen, Hide-Zustände</value>
</data>
<data name="Settings_Card_Chat_Title" xml:space="preserve">
<value>Chat</value>
</data>
<data name="Settings_Card_Chat_Subtext" xml:space="preserve">
<value>Chat-Verhalten, Emotes, Auto-Tells</value>
</data>
<data name="Settings_Card_Tabs_Title" xml:space="preserve">
<value>Tabs</value>
</data>
<data name="Settings_Card_Tabs_Subtext" xml:space="preserve">
<value>Tab-Layout, Kanäle, eigene Tabs</value>
</data>
<data name="Settings_Card_Privacy_Title" xml:space="preserve">
<value>Datenschutz</value>
</data>
<data name="Settings_Card_Privacy_Subtext" xml:space="preserve">
<value>Filter, Aufbewahrung, Bereinigung, Export</value>
</data>
<data name="Settings_Card_Database_Title" xml:space="preserve">
<value>Datenbank</value>
</data>
<data name="Settings_Card_Database_Subtext" xml:space="preserve">
<value>Speicher, Migration, alte Bereinigung</value>
</data>
<data name="Settings_Card_Information_Title" xml:space="preserve">
<value>Information</value>
</data>
<data name="Settings_Card_Information_Subtext" xml:space="preserve">
<value>Über, Mitwirkende, Support</value>
</data>
<data name="Settings_Tab_Themes" xml:space="preserve">
<value>Themes</value>
</data>
<data name="Settings_Themes_Active" xml:space="preserve">
<value>Aktiv: {0}</value>
</data>
<data name="Settings_Themes_BuiltIns" xml:space="preserve">
<value>Eingebaute Themes</value>
</data>
<data name="Settings_Themes_Custom" xml:space="preserve">
<value>Eigene Themes</value>
</data>
<data name="Settings_Themes_OpenFolder" xml:space="preserve">
<value>Themes-Ordner öffnen</value>
</data>
<data name="Settings_Themes_ExportActive" xml:space="preserve">
<value>Aktives exportieren...</value>
</data>
<data name="Settings_Themes_ApplyChatColors_Hint" xml:space="preserve">
<value>Dieses Theme schlägt eigene Channel-Farben vor.</value>
</data>
<data name="Settings_Themes_ApplyChatColors_Apply" xml:space="preserve">
<value>Übernehmen</value>
</data>
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
<value>Behalten</value>
</data>
</root>
@@ -19,7 +19,7 @@
<value>Enable privacy filter</value>
</data>
<data name="Privacy_FilterEnabled_Description" xml:space="preserve">
<value>When enabled, only messages from whitelisted channels are persisted to the database. Disabling restores upstream ChatTwo behavior (everything except battle messages is stored).</value>
<value>When enabled, only messages from whitelisted channels are persisted to the database. Disabling restores the original behavior (everything except battle messages is stored).</value>
</data>
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve">
<value>The filter only controls what is written to the local database. The chat log itself keeps showing every message live, disallowed channels just stop being saved. Use the channel hide options in your in-game chat tabs if you want to remove channels from the visible chat.</value>
@@ -211,7 +211,7 @@
<value>Full History</value>
</data>
<data name="Wizard_Profile_FullHistory_Description" xml:space="preserve">
<value>Disables the privacy filter entirely. Stores everything except battle logs, just like upstream Chat 2. Retention is OFF, history grows forever.</value>
<value>Disables the privacy filter entirely. Stores everything except battle logs (the original full-history behavior). Retention is OFF, history grows forever.</value>
</data>
<data name="Wizard_Profile_FullHistory_GdprWarning" xml:space="preserve">
<value>GDPR notice: storing third-party messages (Say/Shout/Yell of strangers, NPC dialogue with player names, etc.) for an unlimited time may exceed the personal/household exemption (Art. 2(2)(c)). Use this profile only if you have a clear reason to keep the full archive.</value>
@@ -562,7 +562,7 @@
<value>If you use multiple linkshells, the maintainer recommends one tab per shell for cleaner readability. Duplicate this tab and narrow the channel selection per copy.</value>
</data>
<data name="ChatColourPresets_Default" xml:space="preserve">
<value>ChatTwo Default</value>
<value>Klassik (Chat 2 Default)</value>
</data>
<data name="ChatColourPresets_HighContrast" xml:space="preserve">
<value>High-Contrast</value>
@@ -591,6 +591,12 @@
<data name="Settings_Window_PopOutInputEnabled_Description" xml:space="preserve">
<value>Master switch: lets you type and send messages directly inside every pop-out window (including auto-tell tabs). Channel changes inside a pop-out apply globally just like in the main window; the text buffer and history cursor stay independent per pop-out.</value>
</data>
<data name="Settings_Window_ResetPosition_Name" xml:space="preserve">
<value>Reset Window Position</value>
</data>
<data name="Settings_Window_ResetPosition_Description" xml:space="preserve">
<value>Snaps the chat window and every active pop-out back to the primary monitor's top-left corner. Useful when a window has drifted off-screen after a display layout change (monitor disconnected, resolution changed). The plugin also runs an automatic bounds check once per session — this button is the manual backup if anything still ends up unreachable.</value>
</data>
<data name="Popout_v060_HintText" xml:space="preserve">
<value>New in v0.6.0: you can type directly inside pop-out windows. Toggle the master switch in the window settings to enable it.</value>
</data>
@@ -609,4 +615,94 @@
<data name="Hint_v061_PopOutHeader_OpenSettings" xml:space="preserve">
<value>Open Settings</value>
</data>
<data name="ChatTwoConflictTitle" xml:space="preserve">
<value>Hellion Chat cannot start while Chat 2 is loaded.</value>
</data>
<data name="ChatTwoConflictBody" xml:space="preserve">
<value>Hellion Chat is a standalone fork of Chat 2. Both plugins replace the same in-game chat window and would conflict at runtime if loaded together.</value>
</data>
<data name="ChatTwoConflictAction" xml:space="preserve">
<value>Disable Chat 2 in /xlplugins, then re-enable Hellion Chat.</value>
</data>
<data name="Settings_Card_General_Title" xml:space="preserve">
<value>General</value>
</data>
<data name="Settings_Card_General_Subtext" xml:space="preserve">
<value>Language and basic behaviour</value>
</data>
<data name="Settings_Card_Appearance_Title" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="Settings_Card_Appearance_Subtext" xml:space="preserve">
<value>Window opacity, fonts, motion</value>
</data>
<data name="Settings_Card_Themes_Title" xml:space="preserve">
<value>Themes</value>
</data>
<data name="Settings_Card_Themes_Subtext" xml:space="preserve">
<value>Choose a theme or import your own</value>
</data>
<data name="Settings_Card_Window_Title" xml:space="preserve">
<value>Window</value>
</data>
<data name="Settings_Card_Window_Subtext" xml:space="preserve">
<value>Window position, frame, hide states</value>
</data>
<data name="Settings_Card_Chat_Title" xml:space="preserve">
<value>Chat</value>
</data>
<data name="Settings_Card_Chat_Subtext" xml:space="preserve">
<value>Chat behaviour, emotes, auto-tells</value>
</data>
<data name="Settings_Card_Tabs_Title" xml:space="preserve">
<value>Tabs</value>
</data>
<data name="Settings_Card_Tabs_Subtext" xml:space="preserve">
<value>Tab layout, channels, custom tabs</value>
</data>
<data name="Settings_Card_Privacy_Title" xml:space="preserve">
<value>Privacy</value>
</data>
<data name="Settings_Card_Privacy_Subtext" xml:space="preserve">
<value>Filter, retention, cleanup, export</value>
</data>
<data name="Settings_Card_Database_Title" xml:space="preserve">
<value>Database</value>
</data>
<data name="Settings_Card_Database_Subtext" xml:space="preserve">
<value>Storage, migration, legacy cleanup</value>
</data>
<data name="Settings_Card_Information_Title" xml:space="preserve">
<value>Information</value>
</data>
<data name="Settings_Card_Information_Subtext" xml:space="preserve">
<value>About, credits, support</value>
</data>
<data name="Settings_Tab_Themes" xml:space="preserve">
<value>Themes</value>
</data>
<data name="Settings_Themes_Active" xml:space="preserve">
<value>Active: {0}</value>
</data>
<data name="Settings_Themes_BuiltIns" xml:space="preserve">
<value>Built-in themes</value>
</data>
<data name="Settings_Themes_Custom" xml:space="preserve">
<value>Custom themes</value>
</data>
<data name="Settings_Themes_OpenFolder" xml:space="preserve">
<value>Open themes folder</value>
</data>
<data name="Settings_Themes_ExportActive" xml:space="preserve">
<value>Export active...</value>
</data>
<data name="Settings_Themes_ApplyChatColors_Hint" xml:space="preserve">
<value>This theme suggests its own chat channel colours.</value>
</data>
<data name="Settings_Themes_ApplyChatColors_Apply" xml:space="preserve">
<value>Apply</value>
</data>
<data name="Settings_Themes_ApplyChatColors_Keep" xml:space="preserve">
<value>Keep current</value>
</data>
</root>
@@ -7,7 +7,7 @@
// </auto-generated>
//------------------------------------------------------------------------------
namespace ChatTwo.Resources {
namespace HellionChat.Resources {
using System;
@@ -38,7 +38,7 @@ namespace ChatTwo.Resources {
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ChatTwo.Resources.Language", typeof(Language).Assembly);
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HellionChat.Resources.Language", typeof(Language).Assembly);
resourceMan = temp;
}
return resourceMan;
@@ -2148,6 +2148,24 @@ namespace ChatTwo.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to The channel selector button next to the input field is tinted with the currently active channel's colour. Matches the tinting of the input text itself..
/// </summary>
internal static string Options_ColorSelectedInputChannelButton_Description {
get {
return ResourceManager.GetString("Options_ColorSelectedInputChannelButton_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Tint channel selector with channel colour.
/// </summary>
internal static string Options_ColorSelectedInputChannelButton_Name {
get {
return ResourceManager.GetString("Options_ColorSelectedInputChannelButton_Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Chat colours.
/// </summary>
@@ -2661,6 +2679,24 @@ namespace ChatTwo.Resources {
}
}
/// <summary>
/// Looks up a localized string similar to Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again..
/// </summary>
internal static string Options_HideInNewGamePlusMenu_Description {
get {
return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hide while New Game+ menu is open.
/// </summary>
internal static string Options_HideInNewGamePlusMenu_Name {
get {
return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Name", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hide {0} during loading screens..
/// </summary>
@@ -208,6 +208,12 @@
<data name="Options_ChatColours_Import">
<value>Vom Spiel importieren</value>
</data>
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
<value>Channel-Auswahl-Knopf in Channel-Farbe</value>
</data>
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
<value>Der Channel-Auswahl-Knopf neben dem Eingabefeld bekommt die Farbe des aktuell aktiven Channels. Konsistent zur Färbung des Eingabetextes selbst.</value>
</data>
<data name="Options_Tabs_Tab">
<value>Kanäle</value>
</data>
@@ -1190,6 +1196,12 @@ Sie wurden gewarnt.</value>
<data name="Options_HideInBattle_Description" xml:space="preserve">
<value>Blende den Chat während der Kämpfe aus.</value>
</data>
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
<value>Während des New-Game+ Menüs ausblenden</value>
</data>
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
<value>Blendet den Chat aus, solange das New-Game+ Menü geöffnet ist. Schließen des Menüs blendet den Chat wieder ein.</value>
</data>
<data name="Options_Emote_EmoteStats" xml:space="preserve">
<value>Emote-Statistik</value>
</data>
@@ -208,6 +208,12 @@
<data name="Options_ChatColours_Import">
<value>Import from game</value>
</data>
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
<value>Tint channel selector with channel colour</value>
</data>
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
<value>The channel selector button next to the input field is tinted with the currently active channel's colour. Matches the tinting of the input text itself.</value>
</data>
<data name="Options_Tabs_Tab">
<value>Tabs</value>
</data>
@@ -1189,6 +1195,12 @@
<data name="Options_HideInBattle_Description" xml:space="preserve">
<value>Hide the chat during battles.</value>
</data>
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
<value>Hide while New Game+ menu is open</value>
</data>
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
<value>Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again.</value>
</data>
<data name="Options_Emote_EmoteStats" xml:space="preserve">
<value>Emote Stats</value>
</data>
@@ -1364,7 +1364,7 @@
<value>启动</value>
</data>
<data name="Webinterface_Start_Success" xml:space="preserve">
<value>网页界面已停止。</value>
<value>网页界面已启动。</value>
</data>
<data name="Webinterface_Start_Failed" xml:space="preserve">
<value>网页界面启动失败。检查 /xllog 获取更多信息。</value>
+3 -3
View File
@@ -2,7 +2,7 @@
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace ChatTwo;
namespace HellionChat;
public static class Sheets
{
@@ -37,7 +37,7 @@ public static class Sheets
public static IEnumerable<World> WorldsOnDatacenter(IPlayerCharacter character)
{
var dcRow = character.HomeWorld.Value.DataCenter.Value.Region.RowId;
return WorldSheet.Where(world => world.IsPublic && world.DataCenter.Value.Region.RowId == dcRow);
var dcRow = character.HomeWorld.Value.DataCenter.RowId;
return WorldSheet.Where(world => world.IsPublic && world.DataCenter.RowId == dcRow);
}
}
@@ -0,0 +1,50 @@
using HellionChat.Util;
namespace HellionChat.Themes.Builtin;
internal static class Chat2Classic
{
public const string Slug = "chat2-classic";
public static Theme Build() => new(
Slug: Slug,
Name: "Chat 2 Klassik",
Author: "Upstream (Infi & Anna)",
Description: "Steel-blue accents on neutral dark grey, eckige Kanten. Vertraut für ChatTwo-Veteranen.",
Colors: new ThemeColors(
PrimaryDark: ColourUtil.HexToRgba("#3D6E92"),
Primary: ColourUtil.HexToRgba("#4682B4"),
PrimaryLight: ColourUtil.HexToRgba("#5C9DC8"),
PrimaryGlow: ColourUtil.HexToRgba("#4682B466"),
AccentDark: ColourUtil.HexToRgba("#3D6E92"),
Accent: ColourUtil.HexToRgba("#4682B4"),
AccentLight: ColourUtil.HexToRgba("#5C9DC8"),
Identity: ColourUtil.HexToRgba("#4682B4"),
WindowBg: ColourUtil.HexToRgba("#0F0F0FF2"),
ChildBg: ColourUtil.HexToRgba("#141414"),
FrameBg: ColourUtil.HexToRgba("#1A1A1A"),
Surface: ColourUtil.HexToRgba("#202020"),
SurfaceHover: ColourUtil.HexToRgba("#2C2C2C"),
Border: ColourUtil.HexToRgba("#404040"),
TextPrimary: ColourUtil.HexToRgba("#E6E6E6"),
TextMuted: ColourUtil.HexToRgba("#999999"),
TextDim: ColourUtil.HexToRgba("#666666"),
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
StatusInfo: ColourUtil.HexToRgba("#4682B4")
),
Layout: new ThemeLayout(
WindowRounding: 0f, ChildRounding: 0f, PopupRounding: 0f,
FrameRounding: 0f, GrabRounding: 0f, TabRounding: 0f,
ScrollbarRounding: 0f, WindowBorderSize: 1f, FrameBorderSize: 1f
),
Typography: new ThemeTypography(),
IsBuiltIn: true
);
}
@@ -0,0 +1,77 @@
using HellionChat.Util;
namespace HellionChat.Themes.Builtin;
internal static class EventHorizon
{
public const string Slug = "event-horizon";
public static Theme Build() => new(
Slug: Slug,
Name: "Event Horizon",
Author: "Hellion Online Media",
Description: "Cosmic Purple auf Near-Black. Deep-Space-Stimmung.",
Colors: new ThemeColors(
PrimaryDark: ColourUtil.HexToRgba("#7B3FCF"),
Primary: ColourUtil.HexToRgba("#9D5CFF"),
PrimaryLight: ColourUtil.HexToRgba("#B585FF"),
PrimaryGlow: ColourUtil.HexToRgba("#9D5CFF99"),
AccentDark: ColourUtil.HexToRgba("#C9982E"),
Accent: ColourUtil.HexToRgba("#E0AB36"),
AccentLight: ColourUtil.HexToRgba("#F2C25C"),
Identity: ColourUtil.HexToRgba("#9D5CFF"),
WindowBg: ColourUtil.HexToRgba("#040308"),
ChildBg: ColourUtil.HexToRgba("#0A081A"),
FrameBg: ColourUtil.HexToRgba("#140F23"),
Surface: ColourUtil.HexToRgba("#1B1530"),
SurfaceHover: ColourUtil.HexToRgba("#251D40"),
Border: ColourUtil.HexToRgba("#9D5CFF44"),
TextPrimary: ColourUtil.HexToRgba("#E6E0F5"),
TextMuted: ColourUtil.HexToRgba("#9890B5"),
TextDim: ColourUtil.HexToRgba("#5A5570"),
StatusSuccess: ColourUtil.HexToRgba("#26A269"),
StatusDanger: ColourUtil.HexToRgba("#ED333B"),
StatusWarning: ColourUtil.HexToRgba("#E0AB36"),
StatusInfo: ColourUtil.HexToRgba("#9D5CFF")
),
Layout: new ThemeLayout(
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
),
Typography: new ThemeTypography(),
IsBuiltIn: true,
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
{
// Event Horizon — Cosmic-Purple-Drift: helle Pastelle bekommen
// Lavender-Tinte, Akzent-Channels (Tell) ziehen Richtung Magenta-
// Lila. Channel-Identität bleibt klar erkennbar.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E6E0F5"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F2C25C"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FF9050"),
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#E090FF"),
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#E090FF"),
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#90A0FF"),
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFAA80"),
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#9090E8"),
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A0E090"),
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#90A0FF"),
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A0E090"),
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0B070"),
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F2C25C"),
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0B0"),
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#90A0FF"),
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#B585FF"),
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#E090FF"),
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A0F0"),
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E0B870"),
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E0B870"),
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9890B5"),
})
);
}
@@ -0,0 +1,76 @@
using HellionChat.Util;
namespace HellionChat.Themes.Builtin;
internal static class HellionArctic
{
public const string Slug = "hellion-arctic";
public static Theme Build() => new(
Slug: Slug,
Name: "Hellion Arctic",
Author: "Hellion Online Media",
Description: "Arctic Cyan + Ember Glow on industrial slate. Plugin default.",
Colors: new ThemeColors(
PrimaryDark: ColourUtil.HexToRgba("#0097A7"),
Primary: ColourUtil.HexToRgba("#00BED2"),
PrimaryLight: ColourUtil.HexToRgba("#4DD9E8"),
PrimaryGlow: ColourUtil.HexToRgba("#00BED299"),
AccentDark: ColourUtil.HexToRgba("#E85D04"),
Accent: ColourUtil.HexToRgba("#F97316"),
AccentLight: ColourUtil.HexToRgba("#FB923C"),
Identity: ColourUtil.HexToRgba("#0097A7"),
WindowBg: ColourUtil.HexToRgba("#070B12"),
ChildBg: ColourUtil.HexToRgba("#0C1220"),
FrameBg: ColourUtil.HexToRgba("#141E30"),
Surface: ColourUtil.HexToRgba("#1A2538"),
SurfaceHover: ColourUtil.HexToRgba("#22303F"),
Border: ColourUtil.HexToRgba("#00BED266"),
TextPrimary: ColourUtil.HexToRgba("#E6F4F1"),
TextMuted: ColourUtil.HexToRgba("#8FA3B5"),
TextDim: ColourUtil.HexToRgba("#566273"),
StatusSuccess: ColourUtil.HexToRgba("#5CB85C"),
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
StatusWarning: ColourUtil.HexToRgba("#F0AD4E"),
StatusInfo: ColourUtil.HexToRgba("#00BED2")
),
Layout: new ThemeLayout(
WindowRounding: 4f, ChildRounding: 3f, PopupRounding: 3f,
FrameRounding: 2f, GrabRounding: 2f, TabRounding: 2f,
ScrollbarRounding: 2f, WindowBorderSize: 1f, FrameBorderSize: 1f
),
Typography: new ThemeTypography(),
IsBuiltIn: true,
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
{
// Hellion Arctic — FFXIV-Standard mit dezenter Cyan-Tinte in den
// blauen Channels (Party/FC). Channel-Identität bleibt klar.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#FFFFFF"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#FFE066"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#FFA040"),
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#FF99CC"),
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#FF99CC"),
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80C0E8"),
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#FFB870"),
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#4DD9E8"),
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#A8E060"),
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80C0E8"),
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#A8E060"),
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#FFC080"),
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#FFE066"),
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E8A8"),
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80C0E8"),
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A8A0F0"),
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#FF99CC"),
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0F0"),
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C880"),
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C880"),
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#C0C0C0"),
})
);
}
+77
View File
@@ -0,0 +1,77 @@
using HellionChat.Util;
namespace HellionChat.Themes.Builtin;
internal static class MintGrove
{
public const string Slug = "mint-grove";
public static Theme Build() => new(
Slug: Slug,
Name: "Mint Grove",
Author: "Hellion Online Media",
Description: "Mint Green + Honey Amber auf Deep Forest. Naturthemen-tauglich.",
Colors: new ThemeColors(
PrimaryDark: ColourUtil.HexToRgba("#3CB371"),
Primary: ColourUtil.HexToRgba("#5DD39E"),
PrimaryLight: ColourUtil.HexToRgba("#8FE0B8"),
PrimaryGlow: ColourUtil.HexToRgba("#5DD39E99"),
AccentDark: ColourUtil.HexToRgba("#F4C870"),
Accent: ColourUtil.HexToRgba("#F9D580"),
AccentLight: ColourUtil.HexToRgba("#FCDD93"),
Identity: ColourUtil.HexToRgba("#5DD39E"),
WindowBg: ColourUtil.HexToRgba("#0A1410"),
ChildBg: ColourUtil.HexToRgba("#10201A"),
FrameBg: ColourUtil.HexToRgba("#162B22"),
Surface: ColourUtil.HexToRgba("#1E372B"),
SurfaceHover: ColourUtil.HexToRgba("#284335"),
Border: ColourUtil.HexToRgba("#5DD39E55"),
TextPrimary: ColourUtil.HexToRgba("#E8F5EA"),
TextMuted: ColourUtil.HexToRgba("#9BB5A5"),
TextDim: ColourUtil.HexToRgba("#5C6F65"),
StatusSuccess: ColourUtil.HexToRgba("#5DD39E"),
StatusDanger: ColourUtil.HexToRgba("#D9534F"),
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
StatusInfo: ColourUtil.HexToRgba("#5DA9C7")
),
Layout: new ThemeLayout(
WindowRounding: 5f, ChildRounding: 4f, PopupRounding: 4f,
FrameRounding: 3f, GrabRounding: 3f, TabRounding: 3f,
ScrollbarRounding: 3f, WindowBorderSize: 1f, FrameBorderSize: 1f
),
Typography: new ThemeTypography(),
IsBuiltIn: true,
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
{
// Mint Grove — Naturthemen-Tönung: Honey-Amber in Yell-Familie,
// Mint-Drift in NoviceNetwork und Linkshell. Tell-Pink-Identität
// bleibt erhalten für Erkennbarkeit.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#E8F5EA"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F9D580"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F0A050"),
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#F098C8"),
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#F098C8"),
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#80B8D0"),
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B070"),
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#80C8B0"),
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#8FE0B8"),
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#80B8D0"),
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#8FE0B8"),
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC80"),
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F9D580"),
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#80E0A0"),
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#80B8D0"),
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#A89DC0"),
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#F098C8"),
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#D0A8C8"),
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8C088"),
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8C088"),
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9BB5A5"),
})
);
}
@@ -0,0 +1,76 @@
using HellionChat.Util;
namespace HellionChat.Themes.Builtin;
internal static class MoonlitBloom
{
public const string Slug = "moonlit-bloom";
public static Theme Build() => new(
Slug: Slug,
Name: "Moonlit Bloom",
Author: "Hellion Online Media",
Description: "Bloom Magenta + Soft Sage auf Deep Violet Night.",
Colors: new ThemeColors(
PrimaryDark: ColourUtil.HexToRgba("#C957D0"),
Primary: ColourUtil.HexToRgba("#E374E8"),
PrimaryLight: ColourUtil.HexToRgba("#EF8AF4"),
PrimaryGlow: ColourUtil.HexToRgba("#E374E899"),
AccentDark: ColourUtil.HexToRgba("#7AAC5C"),
Accent: ColourUtil.HexToRgba("#9CCB7C"),
AccentLight: ColourUtil.HexToRgba("#B6E297"),
Identity: ColourUtil.HexToRgba("#E374E8"),
WindowBg: ColourUtil.HexToRgba("#0E0C1F"),
ChildBg: ColourUtil.HexToRgba("#15122B"),
FrameBg: ColourUtil.HexToRgba("#1F1A38"),
Surface: ColourUtil.HexToRgba("#28224A"),
SurfaceHover: ColourUtil.HexToRgba("#332B5B"),
Border: ColourUtil.HexToRgba("#E374E844"),
TextPrimary: ColourUtil.HexToRgba("#ECE6F5"),
TextMuted: ColourUtil.HexToRgba("#9A8BB0"),
TextDim: ColourUtil.HexToRgba("#554B6E"),
StatusSuccess: ColourUtil.HexToRgba("#7AAC5C"),
StatusDanger: ColourUtil.HexToRgba("#E85C6A"),
StatusWarning: ColourUtil.HexToRgba("#E8B590"),
StatusInfo: ColourUtil.HexToRgba("#6278FF")
),
Layout: new ThemeLayout(
WindowRounding: 6f, ChildRounding: 5f, PopupRounding: 5f,
FrameRounding: 4f, GrabRounding: 4f, TabRounding: 4f,
ScrollbarRounding: 4f, WindowBorderSize: 1f, FrameBorderSize: 1f
),
Typography: new ThemeTypography(),
IsBuiltIn: true,
ChatColors: new ThemeChatColors(new Dictionary<HellionChat.Code.ChatType, uint>
{
// Moonlit Bloom — Bloom-Magenta-Tönung. Sage-Drift in NoviceNetwork
// und Linkshell4. Tell-Pink-Identität bleibt sichtbar.
[HellionChat.Code.ChatType.Say] = ColourUtil.HexToRgba("#ECE6F5"),
[HellionChat.Code.ChatType.Yell] = ColourUtil.HexToRgba("#F0D080"),
[HellionChat.Code.ChatType.Shout] = ColourUtil.HexToRgba("#F09A60"),
[HellionChat.Code.ChatType.TellIncoming] = ColourUtil.HexToRgba("#EF8AF4"),
[HellionChat.Code.ChatType.TellOutgoing] = ColourUtil.HexToRgba("#EF8AF4"),
[HellionChat.Code.ChatType.Party] = ColourUtil.HexToRgba("#A0B0F0"),
[HellionChat.Code.ChatType.Alliance] = ColourUtil.HexToRgba("#F0B090"),
[HellionChat.Code.ChatType.FreeCompany] = ColourUtil.HexToRgba("#A8C8E8"),
[HellionChat.Code.ChatType.NoviceNetwork] = ColourUtil.HexToRgba("#9CCB7C"),
[HellionChat.Code.ChatType.CrossParty] = ColourUtil.HexToRgba("#A0B0F0"),
[HellionChat.Code.ChatType.Linkshell1] = ColourUtil.HexToRgba("#9CCB7C"),
[HellionChat.Code.ChatType.Linkshell2] = ColourUtil.HexToRgba("#F0BC92"),
[HellionChat.Code.ChatType.Linkshell3] = ColourUtil.HexToRgba("#F0D080"),
[HellionChat.Code.ChatType.Linkshell4] = ColourUtil.HexToRgba("#B6E297"),
[HellionChat.Code.ChatType.Linkshell5] = ColourUtil.HexToRgba("#A0B0F0"),
[HellionChat.Code.ChatType.Linkshell6] = ColourUtil.HexToRgba("#C098D8"),
[HellionChat.Code.ChatType.Linkshell7] = ColourUtil.HexToRgba("#EF8AF4"),
[HellionChat.Code.ChatType.Linkshell8] = ColourUtil.HexToRgba("#E8B0E8"),
[HellionChat.Code.ChatType.CustomEmote] = ColourUtil.HexToRgba("#E8B590"),
[HellionChat.Code.ChatType.StandardEmote] = ColourUtil.HexToRgba("#E8B590"),
[HellionChat.Code.ChatType.Echo] = ColourUtil.HexToRgba("#9A8BB0"),
})
);
}
@@ -0,0 +1,41 @@
{
"schemaVersion": 1,
"slug": "example-custom",
"name": "Example Custom",
"author": "You",
"description": "Starting template — duplicate, rename, edit colors and reload.",
"colors": {
"primaryDark": "#0097A7",
"primary": "#00BED2",
"primaryLight": "#4DD9E8",
"primaryGlow": "#00BED299",
"accentDark": "#E85D04",
"accent": "#F97316",
"accentLight": "#FB923C",
"identity": "#0097A7",
"windowBg": "#070B12",
"childBg": "#0C1220",
"frameBg": "#141E30",
"surface": "#1A2538",
"surfaceHover": "#22303F",
"border": "#00BED266",
"textPrimary": "#E6F4F1",
"textMuted": "#8FA3B5",
"textDim": "#566273",
"statusSuccess": "#5CB85C",
"statusDanger": "#D9534F",
"statusWarning": "#F0AD4E",
"statusInfo": "#00BED2"
},
"layout": {
"windowRounding": 4,
"childRounding": 3,
"popupRounding": 3,
"frameRounding": 2,
"grabRounding": 2,
"tabRounding": 2,
"scrollbarRounding": 2,
"windowBorderSize": 1,
"frameBorderSize": 1
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace HellionChat.Themes;
public sealed record Theme(
string Slug,
string Name,
string Author,
string Description,
ThemeColors Colors,
ThemeLayout Layout,
ThemeTypography Typography,
bool IsBuiltIn,
ThemeChatColors? ChatColors = null
);
+11
View File
@@ -0,0 +1,11 @@
using HellionChat.Code;
namespace HellionChat.Themes;
// Optional pro Theme. Wenn ein Theme ChatColors mitliefert, kann der
// User sie per Klick im Themes-Tab auf Configuration.ChatColours anwenden.
// Ein Theme ohne ChatColors (z.B. chat2-classic) lässt die User-Channel-
// Farben unverändert.
public sealed record ThemeChatColors(
IReadOnlyDictionary<ChatType, uint> Channels
);
+31
View File
@@ -0,0 +1,31 @@
namespace HellionChat.Themes;
// Color-Werte als 0xRRGGBBAA, RgbaToAbgr handled den Byte-Swap zu ImGui.
public sealed record ThemeColors(
uint PrimaryDark,
uint Primary,
uint PrimaryLight,
uint PrimaryGlow,
uint AccentDark,
uint Accent,
uint AccentLight,
uint Identity,
uint WindowBg,
uint ChildBg,
uint FrameBg,
uint Surface,
uint SurfaceHover,
uint Border,
uint TextPrimary,
uint TextMuted,
uint TextDim,
uint StatusSuccess,
uint StatusDanger,
uint StatusWarning,
uint StatusInfo
);
+131
View File
@@ -0,0 +1,131 @@
using System.Text.Json;
using HellionChat.Util;
namespace HellionChat.Themes;
internal static class ThemeJsonLoader
{
public const int SupportedSchemaVersion = 1;
public static Theme LoadFromString(string json)
{
if (string.IsNullOrWhiteSpace(json))
throw new FormatException("Theme JSON is empty");
JsonDocument doc;
try { doc = JsonDocument.Parse(json); }
catch (JsonException ex) { throw new FormatException("Theme JSON is not valid JSON", ex); }
using (doc)
{
var root = doc.RootElement;
var schemaVersion = ReadInt(root, "schemaVersion");
if (schemaVersion != SupportedSchemaVersion)
throw new FormatException($"Unsupported schemaVersion {schemaVersion}; expected {SupportedSchemaVersion}");
var slug = ReadString(root, "slug");
var name = ReadString(root, "name");
var author = ReadString(root, "author");
var description = ReadString(root, "description");
var colors = ReadColors(root.GetProperty("colors"));
var layout = ReadLayout(root.GetProperty("layout"));
ThemeChatColors? chatColors = null;
if (root.TryGetProperty("chatChannels", out var ch) && ch.ValueKind == JsonValueKind.Object)
chatColors = ReadChatColors(ch);
return new Theme(slug, name, author, description, colors, layout, new ThemeTypography(), IsBuiltIn: false, ChatColors: chatColors);
}
}
private static ThemeChatColors ReadChatColors(JsonElement el)
{
var dict = new Dictionary<HellionChat.Code.ChatType, uint>();
foreach (var prop in el.EnumerateObject())
{
// Property-Name ist der ChatType-Name als String (z.B. "Say", "Tell"),
// Value ist Hex wie bei den Theme-Colors. Unbekannte Channel-Names
// werden still übersprungen — Forward-Compat falls SE neue Channels
// einführt.
if (!Enum.TryParse<HellionChat.Code.ChatType>(prop.Name, ignoreCase: true, out var channel))
continue;
if (prop.Value.ValueKind != JsonValueKind.String)
continue;
var hex = prop.Value.GetString();
if (string.IsNullOrWhiteSpace(hex))
continue;
dict[channel] = HellionChat.Util.ColourUtil.HexToRgba(hex);
}
return new ThemeChatColors(dict);
}
public static Theme LoadFromFile(string path)
{
var json = File.ReadAllText(path);
return LoadFromString(json);
}
private static ThemeColors ReadColors(JsonElement el) => new(
PrimaryDark: ColourUtil.HexToRgba(ReadString(el, "primaryDark")),
Primary: ColourUtil.HexToRgba(ReadString(el, "primary")),
PrimaryLight: ColourUtil.HexToRgba(ReadString(el, "primaryLight")),
PrimaryGlow: ColourUtil.HexToRgba(ReadString(el, "primaryGlow")),
AccentDark: ColourUtil.HexToRgba(ReadString(el, "accentDark")),
Accent: ColourUtil.HexToRgba(ReadString(el, "accent")),
AccentLight: ColourUtil.HexToRgba(ReadString(el, "accentLight")),
Identity: ColourUtil.HexToRgba(ReadString(el, "identity")),
WindowBg: ColourUtil.HexToRgba(ReadString(el, "windowBg")),
ChildBg: ColourUtil.HexToRgba(ReadString(el, "childBg")),
FrameBg: ColourUtil.HexToRgba(ReadString(el, "frameBg")),
Surface: ColourUtil.HexToRgba(ReadString(el, "surface")),
SurfaceHover: ColourUtil.HexToRgba(ReadString(el, "surfaceHover")),
Border: ColourUtil.HexToRgba(ReadString(el, "border")),
TextPrimary: ColourUtil.HexToRgba(ReadString(el, "textPrimary")),
TextMuted: ColourUtil.HexToRgba(ReadString(el, "textMuted")),
TextDim: ColourUtil.HexToRgba(ReadString(el, "textDim")),
StatusSuccess: ColourUtil.HexToRgba(ReadString(el, "statusSuccess")),
StatusDanger: ColourUtil.HexToRgba(ReadString(el, "statusDanger")),
StatusWarning: ColourUtil.HexToRgba(ReadString(el, "statusWarning")),
StatusInfo: ColourUtil.HexToRgba(ReadString(el, "statusInfo"))
);
private static ThemeLayout ReadLayout(JsonElement el) => new(
WindowRounding: ReadFloat(el, "windowRounding"),
ChildRounding: ReadFloat(el, "childRounding"),
PopupRounding: ReadFloat(el, "popupRounding"),
FrameRounding: ReadFloat(el, "frameRounding"),
GrabRounding: ReadFloat(el, "grabRounding"),
TabRounding: ReadFloat(el, "tabRounding"),
ScrollbarRounding: ReadFloat(el, "scrollbarRounding"),
WindowBorderSize: ReadFloat(el, "windowBorderSize"),
FrameBorderSize: ReadFloat(el, "frameBorderSize")
);
private static string ReadString(JsonElement el, string name)
{
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.String)
throw new FormatException($"Theme JSON missing string property '{name}'");
return v.GetString() ?? throw new FormatException($"Theme JSON property '{name}' is null");
}
private static int ReadInt(JsonElement el, string name)
{
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number)
throw new FormatException($"Theme JSON missing number property '{name}'");
return v.GetInt32();
}
private static float ReadFloat(JsonElement el, string name)
{
if (!el.TryGetProperty(name, out var v) || v.ValueKind != JsonValueKind.Number)
throw new FormatException($"Theme JSON missing number property '{name}'");
return (float)v.GetDouble();
}
}
+73
View File
@@ -0,0 +1,73 @@
using System.Text.Json;
namespace HellionChat.Themes;
internal static class ThemeJsonWriter
{
public static string Serialize(Theme theme)
{
using var ms = new MemoryStream();
using (var writer = new Utf8JsonWriter(ms, new JsonWriterOptions { Indented = true }))
{
writer.WriteStartObject();
writer.WriteNumber("schemaVersion", ThemeJsonLoader.SupportedSchemaVersion);
writer.WriteString("slug", theme.Slug);
writer.WriteString("name", theme.Name);
writer.WriteString("author", theme.Author);
writer.WriteString("description", theme.Description);
writer.WriteStartObject("colors");
WriteColor(writer, "primaryDark", theme.Colors.PrimaryDark);
WriteColor(writer, "primary", theme.Colors.Primary);
WriteColor(writer, "primaryLight", theme.Colors.PrimaryLight);
WriteColor(writer, "primaryGlow", theme.Colors.PrimaryGlow);
WriteColor(writer, "accentDark", theme.Colors.AccentDark);
WriteColor(writer, "accent", theme.Colors.Accent);
WriteColor(writer, "accentLight", theme.Colors.AccentLight);
WriteColor(writer, "identity", theme.Colors.Identity);
WriteColor(writer, "windowBg", theme.Colors.WindowBg);
WriteColor(writer, "childBg", theme.Colors.ChildBg);
WriteColor(writer, "frameBg", theme.Colors.FrameBg);
WriteColor(writer, "surface", theme.Colors.Surface);
WriteColor(writer, "surfaceHover", theme.Colors.SurfaceHover);
WriteColor(writer, "border", theme.Colors.Border);
WriteColor(writer, "textPrimary", theme.Colors.TextPrimary);
WriteColor(writer, "textMuted", theme.Colors.TextMuted);
WriteColor(writer, "textDim", theme.Colors.TextDim);
WriteColor(writer, "statusSuccess", theme.Colors.StatusSuccess);
WriteColor(writer, "statusDanger", theme.Colors.StatusDanger);
WriteColor(writer, "statusWarning", theme.Colors.StatusWarning);
WriteColor(writer, "statusInfo", theme.Colors.StatusInfo);
writer.WriteEndObject();
writer.WriteStartObject("layout");
writer.WriteNumber("windowRounding", theme.Layout.WindowRounding);
writer.WriteNumber("childRounding", theme.Layout.ChildRounding);
writer.WriteNumber("popupRounding", theme.Layout.PopupRounding);
writer.WriteNumber("frameRounding", theme.Layout.FrameRounding);
writer.WriteNumber("grabRounding", theme.Layout.GrabRounding);
writer.WriteNumber("tabRounding", theme.Layout.TabRounding);
writer.WriteNumber("scrollbarRounding", theme.Layout.ScrollbarRounding);
writer.WriteNumber("windowBorderSize", theme.Layout.WindowBorderSize);
writer.WriteNumber("frameBorderSize", theme.Layout.FrameBorderSize);
writer.WriteEndObject();
if (theme.ChatColors is { Channels.Count: > 0 } cc)
{
writer.WriteStartObject("chatChannels");
foreach (var kvp in cc.Channels)
writer.WriteString(kvp.Key.ToString(), $"#{kvp.Value:X8}");
writer.WriteEndObject();
}
writer.WriteEndObject();
}
return System.Text.Encoding.UTF8.GetString(ms.ToArray());
}
private static void WriteColor(Utf8JsonWriter writer, string key, uint rgba)
{
writer.WriteString(key, $"#{rgba:X8}");
}
}
+14
View File
@@ -0,0 +1,14 @@
namespace HellionChat.Themes;
// Layout-Werte spiegeln die ImGuiStyleVar-Slots, die HellionStyle pusht.
public sealed record ThemeLayout(
float WindowRounding,
float ChildRounding,
float PopupRounding,
float FrameRounding,
float GrabRounding,
float TabRounding,
float ScrollbarRounding,
float WindowBorderSize,
float FrameBorderSize
);
+96
View File
@@ -0,0 +1,96 @@
using HellionChat.Themes.Builtin;
namespace HellionChat.Themes;
public sealed class ThemeRegistry
{
public const string DefaultSlug = HellionArctic.Slug;
private readonly Dictionary<string, Theme> _builtIns;
private readonly Dictionary<string, (Theme Theme, DateTime Stamp)> _customCache = new(StringComparer.OrdinalIgnoreCase);
private readonly string? _customThemesDir;
private Theme _active;
public ThemeRegistry(string? customThemesDir = null)
{
_builtIns = new Dictionary<string, Theme>(StringComparer.OrdinalIgnoreCase)
{
{ HellionArctic.Slug, HellionArctic.Build() },
{ Chat2Classic.Slug, Chat2Classic.Build() },
{ EventHorizon.Slug, EventHorizon.Build() },
{ MoonlitBloom.Slug, MoonlitBloom.Build() },
{ MintGrove.Slug, MintGrove.Build() },
};
_active = _builtIns[DefaultSlug];
_customThemesDir = customThemesDir;
}
public Theme Active => _active;
public Theme Get(string slug)
{
if (_builtIns.TryGetValue(slug, out var b)) return b;
var custom = LoadCustomBySlug(slug);
if (custom != null) return custom;
return _builtIns[DefaultSlug];
}
public IEnumerable<Theme> AllBuiltIns() => _builtIns.Values;
public IEnumerable<Theme> AllCustom() => RefreshCustomCache();
public void Switch(string slug) => _active = Get(slug);
// Custom-Themes werden lazy aus dem Verzeichnis geladen, Cache mit
// LastWriteTime-Token. Eine geänderte JSON wird beim nächsten Lookup
// neu eingelesen.
private Theme? LoadCustomBySlug(string slug)
{
if (_customThemesDir is null) return null;
if (!Directory.Exists(_customThemesDir)) return null;
foreach (var theme in RefreshCustomCache())
if (string.Equals(theme.Slug, slug, StringComparison.OrdinalIgnoreCase))
return theme;
return null;
}
private IEnumerable<Theme> RefreshCustomCache()
{
if (_customThemesDir is null || !Directory.Exists(_customThemesDir))
yield break;
var seenSlugs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var path in Directory.EnumerateFiles(_customThemesDir, "*.json"))
{
Theme? theme = null;
var stamp = File.GetLastWriteTimeUtc(path);
var key = path;
if (_customCache.TryGetValue(key, out var cached) && cached.Stamp == stamp)
{
theme = cached.Theme;
}
else
{
try
{
theme = ThemeJsonLoader.LoadFromFile(path);
_customCache[key] = (theme, stamp);
}
catch (Exception ex)
{
// Logging passiert in Plugin.cs durch den Aufrufer; hier still
// ignorieren, damit ein einzelnes kaputtes JSON nicht alle
// Custom-Themes blockt.
_ = ex;
continue;
}
}
if (theme is not null && seenSlugs.Add(theme.Slug))
yield return theme;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace HellionChat.Themes;
// Optional pro Theme. v1.1.0 nutzt das nicht aktiv; ist als Erweiterungspunkt
// für zukünftige Theme-Slots vorbereitet.
public sealed record ThemeTypography(
float? OverrideGlobalFontSizePt = null,
float? OverrideSymbolsFontSizePt = null
);

Some files were not shown because too many files have changed in this diff Show More