Compare commits

..

249 Commits

Author SHA1 Message Date
JonKazama-Hellion a88ec1714d Merge branch 'feature/v1.2.2-theme-expansion' 2026-05-06 14:07:07 +02:00
JonKazama-Hellion 0110295e7d feat: set Night Blue and Indigo Violet author to Julia Moon 2026-05-06 14:06:14 +02:00
JonKazama-Hellion 9752206996 docs: add forge-post for v1.2.2 2026-05-06 14:04:22 +02:00
JonKazama-Hellion 2f4e4c33ca docs: refresh ROADMAP for 1.2.0/1.2.1/1.2.2 cycle and 1.3.0 next-cycle 2026-05-06 14:02:12 +02:00
JonKazama-Hellion b30b6b135c docs: update THEME-AUTHORING for 1.2.2 themes 2026-05-06 14:01:42 +02:00
JonKazama-Hellion df0844b737 docs: add 1.2.2 entry to CHANGELOG and backfill missing 1.2.1 2026-05-06 14:01:23 +02:00
JonKazama-Hellion 21d703bf0b docs: update HellionChat.yaml description and changelog for 1.2.2 2026-05-06 14:00:11 +02:00
JonKazama-Hellion 4048f0b8d0 chore: bump version to 1.2.2 2026-05-06 13:59:26 +02:00
JonKazama-Hellion 2d0e9ae70c feat: add Hellion Spectrum CVD-safe theme and finalise registry order 2026-05-06 13:59:05 +02:00
JonKazama-Hellion eaf11dcebe feat: add Forge Merchantman built-in theme 2026-05-06 13:57:39 +02:00
JonKazama-Hellion 9bd8262191 feat: add Indigo Violet built-in theme 2026-05-06 13:56:56 +02:00
JonKazama-Hellion ddb00a0836 feat: add Night Blue built-in theme 2026-05-06 13:56:11 +02:00
JonKazama-Hellion aec8ba15f2 docs: add v1.2.1 forge announce post 2026-05-06 11:46:50 +02:00
JonKazama-Hellion c84eae199b merge: v1.2.1 Settings Cleanup 2026-05-06 11:37:07 +02:00
JonKazama-Hellion 9ead8098f5 fix: card-overview subtext wrap + v16 default bumps + chat-colour preset
UI:
- SettingsOverview cards now wrap subtext to two lines (DrawList wrap-
  width) and the card height grew from 96 to 110 px. Single-line
  fitting clipped most of the bilingual subtitles.
- HellionStyle pushes ChildBg with alpha 0 when WindowOpacity < 1.0
  to keep stacked BeginChild layers from compounding the deckgrade
  past what the slider suggests.
- WindowOpacity slider helpmarker now points to Dalamud's per-window
  hamburger menu for opacity / blur / pin / click-through overrides.

UX defaults (v15 → v16 migration adopts new values only when the user
is still on the previous default — bool flips are heuristic, the prior
defaults are from the v1.2.0 cycle and rarely toggled):
- UseCompactDensity false → true (single-line message style is cleaner)
- HideInNewGamePlusMenu false → true (consistent with other hide-flags)
- HideSameTimestamps false → true (cleaner log)
- MaxLinesToRender 5000 → 2500 (mid-range hardware friendlier)
- ChatColours empty → Hellion brand preset (the first-run wizard does
  not offer a preset choice, so fresh installs get the brand colours
  out of the box)
2026-05-06 11:35:59 +02:00
JonKazama-Hellion b190456005 chore: bump version to 1.2.1 and write changelog 2026-05-06 08:46:07 +02:00
JonKazama-Hellion ebc0999a8e refactor: re-sort settings cards thematically for v1.2.1
- Split Appearance into ThemeAndLayout (theme + window-style + timestamps)
  and FontsAndColours (fonts + per-channel colours)
- Merge Database into DataManagement together with Retention/Cleanup/Export
  from Privacy
- Move HistoryPreload from Privacy to Chat → Auto-Tell-Tabs
- Move KeybindMode from General/Language to General/Input
- Drop OverrideStyle, ChosenStyle, WindowAlpha, ShowThemeQuickPicker
- Migration v15 → v16 maps WindowAlpha → WindowOpacity if Opacity at default
- Add card-subtext per overview card so users do not have to guess where
  a setting lives
2026-05-06 08:43:54 +02:00
JonKazama-Hellion c0b3edb20c feat: add v1.2.1 i18n strings for new card layout 2026-05-06 08:31:52 +02:00
JonKazama-Hellion 64cadcf87b fix(release): shrink v1.2.0 changelog under Discord embed-description 4096 cap
Forge-Auto-Announce workflow failed twice on tag push because the
Discord webhook returned 400 — embed.description hit 5346 chars,
which exceeds Discord's hard 4096-per-field limit. The workflow's
own 5500-total cap (V6 check) didn't catch it because it was a
per-field overflow, not a total-payload overflow.

Both yaml changelog block and forge-post DE-body trimmed:
- yaml v1.2.0 EN-block: 3249 → 2104 chars
- forge-post DE-body: 2069 → 1543 chars
- description final: 3675 chars (with ~420 char headroom)
- total payload: 3740 / 5500

Plugin-Manager-facing changelog still covers all v1.2.0 highlights
plus the post-test bug fixes; just denser. Tag will be force-recreated
on this commit so workflow_dispatch picks up the trimmed files from
the v1.2.0 tag tree.

Backlog item: workflow should add a per-field cap check (4096 for
description, 1024 for field values) so future releases fail-fast
locally before hitting Discord.
2026-05-06 00:37:12 +02:00
JonKazama-Hellion 0165cba966 merge: v1.2.0 Layout Refresh
27 commits brought in from feature/v1.2.0-layout-refresh:
- Sidebar/Top-Tabs visual modernisation (icon-only sidebar with
  44px fixed width and tooltip, vertical accent pill, top-tab
  underline pill).
- TabIconMapping with single-source 15-glyph pool, per-tab
  Icon override via Settings → Tabs combobox.
- AutoTellTabTint hash-based icon+color differentiation
  (84 distinct combinations) for parallel tells.
- Bottom status bar (22px): channel/privacy/counts/tells/version.
- Card-Rows as default message render with Compact-Density
  opt-out toggle.
- Pulsing red unread-dot indicator on sidebar tab icons,
  respects Configuration.ReduceMotion.
- Migration v14 → v15: legacy theme fields removed, Appearance
  bindings cleaned to use Themes tab as single source.
- Settings-Save chat-history preservation: UpdateFrom Identifier-
  mapping for persistent tabs, TempTab skip in ClearAllTabs/
  FilterAllTabs, conditional refilter only for filter-relevant
  changes.
- Hellion font (Exo 2) no longer blocks FontSizeV2 adjustment —
  4K user can scale up the variable font.

Tag v1.2.0 sits on the last feature commit (3da550c).
Forge-Auto-Announce-Action triggers on tag push.
2026-05-06 00:18:37 +02:00
JonKazama-Hellion 3da550c2fc fix(fonts): Hellion-Schrift-Toggle blockt Schriftgröße nicht mehr
Settings → Erscheinungsbild → Schriftarten: bei aktiver
'Mitgelieferte Hellion-Schrift (Exo 2) verwenden' war der
Schriftgrößen-Slider ausgegraut und FontSizeV2 wurde im
FontManager auch nicht angewendet — 4K-User konnten den
Plugin-Font nicht hochskalieren.

Exo 2 ist Variable-Font, FontSize ist also problemlos
adjustierbar. Zwei-teiliger Fix:

- Appearance.cs: UseHellionFont rendert jetzt nur FontSizeCombo +
  SymbolsFontSizeCombo, kein Disabled-Wrap mehr. Der Bestand-
  Custom-Font-Stack mit FontsEnabled-Toggle und Font-Choosern
  bleibt exclusive zur Hellion-Schrift, läuft im else-Pfad.
- FontManager.cs RegularFont-Build: SizePt-Source verzweigt
  jetzt auf UseHellionFont — Hellion-Pfad nutzt FontSizeV2,
  Bestand-Pfad nutzt weiter GlobalFontV2.SizePt aus dem
  Custom-Font-Spec.

Reported by Flo 2026-05-06: '4k monitor ... der standart zu klein'.
2026-05-06 00:11:19 +02:00
JonKazama-Hellion 4b43fdb0ad fix(settings): conditional refilter on save — preserve chat history for cosmetic changes
Settings.Save() unconditionally ran ClearAllTabs + FilterAllTabsAsync
after every save. The cycle reloads messages from the DB, which silently
wipes any in-session message that wasn't persisted — Privacy-First
configurations block most channels from the DB, so all unlogged
channels (Allgemein/Say/Yell/Shout under default filters) showed up
empty after every settings save.

New HasFilterRelevantChanges helper compares Mutable to Plugin.Config
across:
- PrivacyFilterEnabled
- PrivacyPersistChannels (HashSet<ChatType>)
- PrivacyPersistUnknownChannels
- FilterIncludePreviousSessions
- per-persistent-tab: Identifier (reorder/swap), SelectedChannels,
  ExtraChatAll, ExtraChatChannels

Refilter only runs if any of those changed. Cosmetic settings (theme,
tab icons, layout, fonts, language) leave the chat log untouched.
Combined with the prior UpdateFrom Identifier-mapping fix and the
TempTab skip in ClearAllTabs/FilterAllTabs, both persistent and
Auto-Tell tabs now fully survive a settings save.

Reported by Flo from in-game testing 2026-05-05/06: 'der allgemein
chat tab z.b immernoch gecleart wird' / 'alle vom plugin nicht
geloggten channel sind dann leer'.

Also updated yaml changelog, docs/CHANGELOG.md and .github/forge-posts/
v1.2.0.md to describe the actual fix shape rather than the partial
UpdateFrom-only fix that preceded it.
2026-05-06 00:05:49 +02:00
JonKazama-Hellion 56621669b2 release(v1.2.0): finalize changelog, yaml notes, forge announce post
Final release-doc pass for v1.2.0:
- yaml changelog extended with the post-T14 polish notes
  (Auto-Tell variety, unread pulse, layout fixes, settings-save
   wipe fixes for both persistent and TempTabs)
- docs/CHANGELOG.md date filled in (2026-05-05) and same polish
  notes added under Added/Fixed sections
- .github/forge-posts/v1.2.0.md created — DE bullet body, picked
  up by forge-announce.yml on tag push (EN side reads the yaml
  changelog block)

1920 chars in the forge post — comfortably under the 5500-char
total cap that the workflow enforces.
2026-05-06 00:00:44 +02:00
JonKazama-Hellion ed2a0f7374 fix(messages): exclude TempTabs from ClearAllTabs and FilterAllTabs
TempTabs (Auto-Tell-Tabs) are session-only and populated directly by
AutoTellTabsService.HandleTell — they have no DB persistence to refilter
from. The Settings-save flow calls ClearAllTabs() + FilterAllTabsAsync()
to rebuild persistent tabs after potential filter changes; this wiped
TempTabs as collateral because their tells aren't in the DB (either
Privacy-filtered out or simply not yet persisted).

Skip TempTabs in both methods so their live-state survives any settings
save. Live tells continue to land via HandleTell, regardless of the
clear/filter cycle.
2026-05-05 23:51:42 +02:00
JonKazama-Hellion 59e86cd8dd fix(config): preserve persistent-tab message history across settings save
UpdateFrom replaced persistent tabs with Tab.Clone()s. Clone deliberately
omits the NonSerialized Messages list to avoid shared mutable state on
disk-load — but on a settings save (Plugin.Config.UpdateFrom(Mutable))
that path means every persistent tab loses its in-session chat history
the moment the user clicks Save.

Capture the live MessageList plus LastSendUnread counter by Identifier
before the replace and restore them onto the cloned tabs. Tab.Clone()
already preserves Identifier so the lookup matches one-to-one for
unchanged tabs. New tabs added in settings get a fresh empty list,
deleted tabs lose their history (both intended).

Reported by Flo in-game 2026-05-05 — chat got wiped on every settings
save during v1.2.0 testing in Limsa.
2026-05-05 23:48:23 +02:00
JonKazama-Hellion a74e3da030 feat(sidebar): subtle pulse animation on unread-dot indicator
Sin-based alpha scaling between 60% and 100% with a 2-second cycle.
Subtle enough to register peripherally without becoming distracting.
Respects Plugin.Config.ReduceMotion (field exists since v1.1.0,
toggle UI lands in v1.3.0) — static render when disabled.
2026-05-05 23:41:31 +02:00
JonKazama-Hellion b8ed2a1ce5 feat(sidebar): per-tell hashed icon variety + unread-dot indicator 2026-05-05 23:36:52 +02:00
JonKazama-Hellion e6c6c02780 feat(sidebar): hash-color tint for auto-tell tabs to disambiguate parallel conversations 2026-05-05 23:27:41 +02:00
JonKazama-Hellion ab9ebedeee fix(sidebar): suppress child background so top padding doesn't show frame fill
The sidebar child window's ChildBg painted the upper top-padding area
(reserved for header-toolbar alignment) with the theme's frame color,
making it look like a stub block above the buttons. Pushing ChildBg
to transparent keeps the buttons floating on the window background.
Vertical separation to the message column stays intact via the
TabTable's BordersInnerV flag.
2026-05-05 23:12:54 +02:00
JonKazama-Hellion 11af4ce4c4 fix(sidebar): top-padding mirrors header toolbar height, restore standard button height
Sidebar buttons sat at the window top while messages began below the
chat header toolbar — vertical mismatch flagged by Flo. Adding a
GetFrameHeightWithSpacing dummy at the top of the sidebar child shifts
the entire button column down to align with the first message row.

Reverted the previous TextLineHeight+4f button shrink (commit 8a78390):
buttons size was fine, only their vertical position needed correction.
2026-05-05 23:10:16 +02:00
JonKazama-Hellion 8a78390a15 fix(sidebar): tighten button height with explicit FramePadding override 2026-05-05 23:03:53 +02:00
JonKazama-Hellion 23e47e06c0 fix(layout): sidebar button height + status-bar version-slot clipping 2026-05-05 22:59:57 +02:00
JonKazama-Hellion ff60576f3c Update copyright notice and asset licensing details 2026-05-05 21:40:28 +02:00
JonKazama-Hellion 5b5bacfc41 Revise upstream sync documentation for clarity
Updated the documentation to clarify the upstream sync workflow, including changes to cherry-picking practices and conflict handling. Added sections on intent, contributing back, and handling upstream changes.
2026-05-05 21:26:53 +02:00
JonKazama-Hellion eb8b7be2f5 Update AI disclosure for HellionChat 2026-05-05 21:13:31 +02:00
JonKazama-Hellion eb05e04f79 Revise contributing guidelines for clarity and updates
Updated the contributing guidelines to reflect changes in project scope, contribution acceptance criteria, and response times. Clarified sections on translations and continuous integration.
2026-05-05 21:07:18 +02:00
JonKazama-Hellion 2f0affcdbb Update SECURITY.md for clarity and formatting 2026-05-05 21:00:06 +02:00
JonKazama-Hellion dfa7c47887 Revise Code of Conduct for clarity and inclusivity
Updated the Code of Conduct to enhance clarity and inclusivity. Added sections on encouraged and restricted behaviors, and improved formatting.
2026-05-05 20:55:24 +02:00
JonKazama-Hellion acf799440e Revise COPYRIGHT file with updated licensing details
Updated copyright information and license details in the COPYRIGHT file, including new copyright holders and clarifications on asset licensing.
2026-05-05 20:25:51 +02:00
JonKazama-Hellion 3e98b9103f chore(release): bump to v1.2.0 — layout refresh 2026-05-05 19:54:56 +02:00
JonKazama-Hellion 4a613f7acb Update NOTICE.md with maintainer details
Added maintenance information for Hellion Forge.
2026-05-05 19:51:51 +02:00
JonKazama-Hellion af5f4d380a feat(config): migration v14 → v15, removed legacy theme fields and Appearance bindings 2026-05-05 19:51:29 +02:00
JonKazama-Hellion ecf1e93a1b Refine language in SUPPORT.md for clarity
Updated phrasing for clarity and consistency throughout the document.
2026-05-05 19:49:26 +02:00
JonKazama-Hellion e404a2e0d9 Refactor privacy notice for clarity and consistency 2026-05-05 19:47:09 +02:00
JonKazama-Hellion d485f5ea1f feat(messages): card-row default render with compact-density opt-out 2026-05-05 19:44:37 +02:00
JonKazama-Hellion b48684ce5a feat(settings): compact density toggle in Appearance 2026-05-05 19:42:57 +02:00
JonKazama-Hellion a11c8bc6e9 feat(statusbar): wire status bar into ChatLogWindow render pipeline 2026-05-05 19:35:52 +02:00
JonKazama-Hellion 985a284e7d feat(statusbar): cached 1Hz status-bar component with format helpers 2026-05-05 19:34:27 +02:00
JonKazama-Hellion e629518550 feat(settings): per-tab icon combobox in Tabs section 2026-05-05 19:28:04 +02:00
JonKazama-Hellion c28c972ae3 revert(top-tabs): drop icon prefix — Dalamud default font lacks FontAwesome codepoints
Tofu-squares rendered in-game (verified by Flo 2026-05-05 19:21).
ImGui TabItem labels render in a single font frame; mixed-font
(FontAwesome icon + default font for tab name) is not possible
without Font-Atlas merging at FontManager level — out of scope
for v1.2.0.

Top-Tabs visual modernization is now driven by the Underline-Pill
alone (T6, kept). Sidebar (icon-only) remains the use case where
icons earn their keep. v1.2.0 Akzeptanzkriterium AC1 wird auf
"Top-Tabs haben Pill-Underline" reduziert.
2026-05-05 19:23:26 +02:00
JonKazama-Hellion bc0f44712f feat(top-tabs): active-tab accent underline pill 2026-05-05 19:18:18 +02:00
JonKazama-Hellion f663cb3c14 feat(top-tabs): icon glyph prefix in tab label 2026-05-05 19:17:59 +02:00
JonKazama-Hellion 5a9c2018b0 feat(sidebar): active-tab vertical accent pill 2026-05-05 19:09:26 +02:00
JonKazama-Hellion a1cdae05d0 feat(sidebar): icon-only tabs with tooltip and 44px fixed width 2026-05-05 19:08:58 +02:00
JonKazama-Hellion c17f5ae516 refactor(tabs): split TabIconGlyphResolver, single-source glyph pool, polish 2026-05-05 19:00:54 +02:00
JonKazama-Hellion a2db8cb639 feat(tabs): TabIconMapping with default-pool plus override resolver 2026-05-05 18:52:47 +02:00
JonKazama-Hellion 507efc8cda feat(tabs): nullable Tab.Icon field for custom glyph override 2026-05-05 18:43:13 +02:00
JonKazama-Hellion 6f3cf2f3ce Revise README for version 1.1.0 updates
Updated README.md for clarity and consistency in language, including changes to versioning and feature descriptions.
2026-05-05 18:39:37 +02:00
JonKazama-Hellion c979a05d6c Update version number to 1.1.0 in README 2026-05-05 18:21:43 +02:00
JonKazama-Hellion c53e453341 Update username in forge-announce workflow 2026-05-05 18:19:58 +02:00
JonKazama-Hellion 2519b413f8 merge: forge-announce workflow
Adds .github/workflows/forge-announce.yml — auto-posts a bilingual
changelog embed to the Hellion Forge #changelog Discord channel on
every vX.Y.Z tag push. DE bullets come from .github/forge-posts/
<tag>.md (frontmatter: subtitle, versionsnatur), EN block from
HellionChat.yaml.

Hard cap 5500 chars total. Major releases that exceed get a clear
manual-post message and stay off the auto-channel.

Decoupled from release.yml — failures in either workflow don't
block the other.
2026-05-05 15:34:13 +02:00
JonKazama-Hellion e5ac4faf7b ci(forge): auto-announce changelog to hellion forge on tag push
New workflow: when a vX.Y.Z tag is pushed (or workflow_dispatch
runs with a tag input), reads .github/forge-posts/<tag>.md for the
DE bullet body plus frontmatter (subtitle, versionsnatur), pulls the
matching English block from HellionChat.yaml, builds the Discord
webhook embed and posts it to the Hellion Forge #changelog channel.

Decoupled from release.yml — a fail here doesn't block the release,
and a fail there doesn't block the announce. Hard caps at 5500 chars
total (title + description + footer); major releases that exceed
that get a clear fail message and stay manual.

Tag is read via env: TAG_NAME and validated against ^v\d+\.\d+\.\d+$
before any string interpolation; frontmatter is regex-parsed with
explicit length caps (subtitle 60, versionsnatur 40). Curl posts the
payload via stdin so the secret never appears in process args.
Single retry on transient 5xx after 30s, hard fail on 4xx.
2026-05-05 15:33:49 +02:00
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
JonKazama-Hellion 0df5819a88 merge: v0.6.1 Pop-Out Discoverability & /tell Auto-Pop-Out
- Visible pop-out icon button in chat header toolbar (right-aligned)
- One-time hint banner introduces toolbar + right-click and the v0.6.1 default flip
- Settings → Chat → Auto-Tell-Tabs → "Open new /tell tabs directly as pop-out"
- PopOutInputEnabled hard-flipped to true via v11 → v12 migration
- Bugfix: pop-out windows of LRU-dropped or logout-stripped temp tabs are now properly torn down (no more ghost windows)
- Bugfix: dead zone below chat input bar when v0.6.0 hint banner was visible (also fixes Jin's report on the v0.6.0 in-pop-out banner)
- CI: fix release.yml YAML parse failure (heredoc footer extracted to .github/release-footer.md), add workflow_dispatch recovery trigger
- README + SUPPORT.md + repo.json + yaml: Hellion Forge Discord link
2026-05-03 17:15:52 +02:00
JonKazama-Hellion 3fbbe8543f ci(release): fix YAML parse failure (heredoc footer broke block-scalar) and add manual recovery trigger 2026-05-03 17:14:05 +02:00
JonKazama-Hellion 03dfb8e3da fix: restore toolbar height subtraction (banner is auto-handled by cursor advance) 2026-05-03 17:02:10 +02:00
JonKazama-Hellion a987e97610 fix: re-add banner height subtraction (lives outside tab container scope) 2026-05-03 17:00:11 +02:00
JonKazama-Hellion ecd46ed630 release: bump version to 0.6.1, add changelog and Hellion Forge Discord link 2026-05-03 16:56:28 +02:00
JonKazama-Hellion f2f7599f81 fix: remove double height subtraction for header toolbar and hint banner 2026-05-03 16:49:24 +02:00
JonKazama-Hellion ac158907ea docs: clarify _v061HintBannerHeight reset semantics 2026-05-03 16:43:12 +02:00
JonKazama-Hellion 9506af49db feat: one-time hint banner introduces v0.6.1 pop-out toolbar and default flip 2026-05-03 16:38:31 +02:00
JonKazama-Hellion c882eac1ca feat: visible pop-out icon button in chat header toolbar 2026-05-03 16:27:28 +02:00
JonKazama-Hellion 7a6b44048a settings: add 'Open new /tell tabs as pop-out' checkbox 2026-05-03 16:17:24 +02:00
JonKazama-Hellion 0d39d59a04 feat: opt-in auto-pop-out for new /tell tabs 2026-05-03 16:13:38 +02:00
JonKazama-Hellion f0e0db55e3 fix: also clean up pop-outs of temp tabs on logout (symmetric to LRU drop) 2026-05-03 16:11:00 +02:00
JonKazama-Hellion f207239d56 fix: clean up pop-out window when auto-tell tab is LRU-dropped 2026-05-03 16:04:57 +02:00
JonKazama-Hellion ccf2ec9f12 i18n: tighten v0.6.1 hint body wording (DE/EN tone consistency) 2026-05-03 16:03:05 +02:00
JonKazama-Hellion aff7a5e7ce i18n: add v0.6.1 strings for hint banner and auto-pop-out toggle (DE/EN) 2026-05-03 15:57:10 +02:00
JonKazama-Hellion cd84ca2b3f migration: clarify v11->v12 log message and trim rotting comment 2026-05-03 15:55:15 +02:00
JonKazama-Hellion 7c645afa1d migration: add v11 -> v12 hard-flip of PopOutInputEnabled to true 2026-05-03 15:51:13 +02:00
JonKazama-Hellion 24c1e0e754 config: bump LatestVersion to 12, flip PopOutInputEnabled default, add v0.6.1 fields 2026-05-03 15:47:05 +02:00
JonKazama-Hellion 9f6a0807d1 docs: update README for v0.6.0 release (Pop-Out Input + Colour Presets) 2026-05-03 13:26:58 +02:00
JonKazama-Hellion 15f83c8b0e merge: v0.6.0 UX Polish — Pop-Out Input + Colour Presets
Two opt-in UX features. Existing users see no change unless they
enable the new toggles.

- Pop-out input: global master switch in Settings → Window → Frame.
  When enabled, every pop-out window grows a compact input bar
  (channel-coloured icon + text input). Independent text buffer and
  history cursor per pop-out; channel changes apply globally.
- Chat colour presets: seven built-ins above the per-channel colour
  list — ChatTwo Default, High-Contrast, Pastell, Dark-Mode-Tuned,
  Hellion (brand), Night Blue (bonus), Indigo Violet (bonus).

Configuration migrates from v10 to v11 with a diagnostic log.
2026-05-03 13:22:11 +02:00
JonKazama-Hellion c7253bdf02 presets: add Night Blue and Indigo Violet bonus presets from KAZAMA themes 2026-05-03 13:19:01 +02:00
JonKazama-Hellion cf10c566dd release: bump version to 0.6.0 with changelog entry 2026-05-03 13:09:11 +02:00
JonKazama-Hellion acfe838bc6 i18n: add v0.6.0 strings for presets, pop-out input and hint banner 2026-05-03 13:06:26 +02:00
JonKazama-Hellion 9e1f559644 config: pivot pop-out input from per-tab to global master switch 2026-05-03 13:01:07 +02:00
JonKazama-Hellion 2c79a67dae settings: add PopOutInputEnabled toggle below PopOut option 2026-05-03 12:51:59 +02:00
JonKazama-Hellion 1687271bfd popout: show one-time hint banner about pop-out input feature 2026-05-03 12:51:35 +02:00
JonKazama-Hellion cb5457ba2e popout: opt-in ChatInputBar with focus-aware keybind routing 2026-05-03 12:50:42 +02:00
JonKazama-Hellion a701f6c103 keybinds: extract DispatchTabDelta helper for upcoming focus-aware routing 2026-05-03 12:49:30 +02:00
JonKazama-Hellion 8cad8651d2 ui: drop auto-translate from compact bar in v0.6.0 (out of scope) 2026-05-03 12:48:46 +02:00
JonKazama-Hellion 61b547606c ui: ChatInputBar compact input with submit and history callback 2026-05-03 12:46:20 +02:00
JonKazama-Hellion 059cfa6e28 ui: ChatInputBar compact channel-selector with ChatColours background 2026-05-03 12:44:35 +02:00
JonKazama-Hellion 71d84e4486 ui: scaffold ChatInputBar component with InputState 2026-05-03 12:43:48 +02:00
JonKazama-Hellion 92301869ed refactor: migrate input history from ChatLogWindow.InputBacklog to InputHistoryService 2026-05-03 12:36:36 +02:00
JonKazama-Hellion c3d06a9c94 services: add InputHistoryService for shared input history 2026-05-03 12:35:14 +02:00
JonKazama-Hellion 911c870e24 presets: rework Hellion preset with Arctic Cyan + Ember Glow brand palette 2026-05-03 12:32:32 +02:00
JonKazama-Hellion 8cda19d993 settings: add chat colour preset buttons with apply logic 2026-05-03 12:26:25 +02:00
JonKazama-Hellion 62621ba855 presets: add five built-in chat colour presets 2026-05-03 12:25:22 +02:00
JonKazama-Hellion 497c259031 config: add v10 to v11 migration with diagnostic log 2026-05-03 12:10:11 +02:00
JonKazama-Hellion 9ad9d2acd2 config: add PopOutInputEnabled and SeenPopOutInputHint, bump LatestVersion to 11 2026-05-03 12:09:51 +02:00
JonKazama-Hellion 1b63765caa docs: community standards, privacy notice and release-body automation
Closes the remaining gaps in GitHub's community-standards check, adds
explicit privacy and dependency documentation matching the plugin's
"DSGVO-by-design" claim, and removes the stale upstream Crowdin
artefact so the repo no longer suggests it ships its own translation
pipeline.

New community-health files:

- CODE_OF_CONDUCT.md: project-specific, short and direct, single
  reporting path to kontakt@hellion-media.de
- CONTRIBUTING.md: scope, accepted vs declined contributions, build
  and test instructions, EUPL-1.2 contribution terms, translation
  policy split between Hellion-specific (here) and upstream strings
  (Chat 2 repo)
- SUPPORT.md: routing for bugs, security, privacy and casual feedback
- .github/PULL_REQUEST_TEMPLATE.md: summary, change-type checklist,
  testing notes, compatibility notes for migrations and manifest
  fields, contribution checklist
- .github/FUNDING.yml: comments-only file, no platforms enabled,
  points donors at the upstream Chat 2 maintainers' Ko-fi pages

New privacy and compliance documentation:

- PRIVACY.md: what the plugin stores locally (config, SQLite,
  EmoteCacheV1), retention defaults, the two outbound network calls
  (BetterTTV API+CDN with ShowEmotes opt-out, Square Enix Lodestone
  font once-off), explicit no-telemetry statement, GDPR
  Art. 15/17/18/20/21 rights mapped to plugin features, third-party
  privacy-policy links
- THIRD_PARTY_NOTICES.md: direct NuGet dependencies with versions
  pinned to v0.5.4 (MessagePack, Microsoft.Data.Sqlite, morelinq,
  Pidgin, SixLabors.ImageSharp under Six Labors Split License 1.0),
  Dalamud SDK and .NET tooling, bundled Exo 2 font (OFL-1.1) and
  plugin icon, network-touch status per component, re-audit commands

Crowdin cleanup:

- crowdin.yml deleted (was upstream Chat 2's project_id 663694,
  pointed at /ChatTwo/Resources/Language.resx, never wired to
  HellionChat strings)
- README, CONTRIBUTING and CODE_OF_CONDUCT no longer suggest
  HellionChat operates a Crowdin project; remaining mentions are
  explicitly framed as upstream Chat 2's workflow

Contact and version consistency:

- Maintainer email switched from maintainer@hellion-media.de to
  kontakt@hellion-media.de in SECURITY.md and NOTICE.md
- README version references updated to 0.5.4 (header, project status
  block) and the update-tag pattern generalised from v0.1.x to v0.X.Y
- bug_report.yml version placeholder bumped to 0.5.4
- Project-documents table added to README footer linking all health
  and reference files in one place

Release-body automation:

- .github/workflows/release.yml now extracts the matching version
  block from ChatTwo/HellionChat.yaml's changelog and combines it
  with a static install / docs footer (custom-repo URL, project
  document links, licence) before passing the result to
  softprops/action-gh-release@v3 via body_path
- Workflow fails fast if no changelog block exists for the tagged
  version, automating the existing "yaml + repo.json + release body
  kept in sync" rule
- Tag value passed via env: TAG_NAME with strict ^v\d+\.\d+\.\d+$
  validation before any string concatenation, so the tag input cannot
  break out into shell evaluation
2026-05-03 10:42:07 +02:00
JonKazama-Hellion 61764459ed merge: WrapText span refactor and 0.5.4 release 2026-05-02 23:57:31 +02:00
JonKazama-Hellion 1b7f2c40e6 fix(security): rebuild WrapText on span and int offsets
The pointer-arithmetic CodeQL alert kept re-firing on each shape of
the previous shallow fix because Encoding.GetBytes is virtual and
every length value derived from its return inherited the taint.
Refactor the routine to thread int offsets through index-based
control flow and only compute pointers inside two small helpers
(CalcWordWrap and DrawText) that take an already-pinned base pointer
plus offsets sourced from local logic, not from any virtual return.

Buffer is now allocated against Encoding.UTF8.GetMaxByteCount via
ArrayPool with a real 16 KiB upper bound, and the encoded length
returned by GetBytes is validated against that ceiling before
anything touches the pointer. Behaviour is byte-identical to v0.5.3,
verified locally with the same input shapes the previous code path
handled.

Slim changelog: trimmed the per-version blocks down to v0.5.1-v0.5.4
plus a link to GitHub releases for older history. The previous block
ran ~9000 characters and was dragging the manifest payload down for
no benefit; users see the latest release block first anyway.
2026-05-02 23:57:26 +02:00
JonKazama-Hellion 93d52ae819 chore(release): bump version to 0.5.3
Single-fix patch to close the CodeQL pointer-arithmetic alert that
v0.5.2 left open. v0.5.2 already shipped, so we tag forward instead
of moving the published tag.
2026-05-02 23:46:26 +02:00
JonKazama-Hellion 48b3d5c6b1 fix(security): validate UTF8 byte buffer length before pointer arithmetic
CodeQL re-opened the unvalidated-pointer-arithmetic alert at the new
textEnd line because Encoding.GetBytes is a virtual method on
Encoding and the returned array's Length is therefore tracked as
untrusted input for pointer arithmetic.

Compute the expected byte count from the same encoder via
GetByteCount and bail out if the actual buffer length does not match.
That is a real consistency check that would catch a maliciously
swapped Encoding.UTF8 instance, not a dead defensive guard. The
empty-split early-out from the previous fix is folded into the same
condition.
2026-05-02 23:42:59 +02:00
JonKazama-Hellion e9a9d8a01c merge: icon packaging fix 2026-05-02 23:33:22 +02:00
JonKazama-Hellion a155a57f33 fix(packaging): icon and image urls now reach the built manifest
Three packaging defects rolled into one fix:

- The custom DalamudPackager.targets override forced HandleImages and
  ImagesPath through the legacy code path. SDK 15 handles images by
  default and the override produced an output manifest with neither
  IconUrl nor ImageUrls populated. Removed.
- The csproj only included images/icon.png explicitly via
  <None Include>, so chatWindow.png and withSimpleTweaks.png never
  reached the build output and never made it into the release ZIP
  either. Switched to a glob include.
- HellionChat.yaml carried no icon_url / image_urls, so even after
  the SDK started writing the manifest correctly, both fields stayed
  unset. Added them pointing at the public raw.githubusercontent
  URLs that already work for the repo.json IconUrl.

Net effect on a fresh release: Dalamud picks up the icon next to the
DLL on dev installs, the plugin-installer card shows the proper
HellionChat logo for users coming through the custom repo, and the
two screenshot images are listed alongside the description so the
plugin installer carousel works the way other Dalamud plugins look.
2026-05-02 23:33:15 +02:00
JonKazama-Hellion 90b83a0690 chore(release): bump version to 0.5.2
Patch release. History-order fix for Auto-Tell-Tabs, three default-
config alignments and two CodeQL security findings closed.
2026-05-02 23:28:35 +02:00
JonKazama-Hellion f10301c3e4 merge: codeql findings #1 and #2 2026-05-02 23:27:12 +02:00
JonKazama-Hellion 8571a936a4 merge: align config defaults with maintainer's live config 2026-05-02 23:27:12 +02:00
JonKazama-Hellion 3f6144836c merge: auto-tell history-order fix 2026-05-02 23:27:12 +02:00
JonKazama-Hellion 53c432a635 fix(security): close codeql findings #1 and #2
Two CodeQL alerts opened against the codeql-manual-build workflow's
first scan. Both real, both small fixes.

#1 Medium / Workflow does not contain permissions
   build.yml runs read-only against the repo (no push, no release
   creation, no API mutations) but never declared a permissions
   block, so the default GITHUB_TOKEN scope applied. Pin to
   contents: read at workflow level. Release and CodeQL workflows
   already have their explicit minimal scopes.

#2 Critical / Unvalidated local pointer arithmetic
   ImGuiUtil.WrappedTextWithPos splits its input on newlines and
   passes each part through Encoding.UTF8.GetBytes inside a fixed
   block. Empty splits (consecutive newlines, blank lines) produced
   a zero-length byte array, fixed gave us a valid pointer, and
   textEnd = text + bytes.Length collapsed onto text. The downstream
   ImGuiNative.CalcWordWrapPositionA calls received identical start
   and end pointers, which is undefined behaviour at the native
   boundary even if it happens to no-op on the current ImGui build.
   Bail before entering the fixed block when bytes.Length == 0 and
   render an empty line for the gap, which is what the original
   text == null guard was trying to do but could never reach inside
   a fixed block over a non-null array.
2026-05-02 23:25:41 +02:00
JonKazama-Hellion 340cadf3b9 chore(config): align defaults with maintainer's live config
Three real-world adjustments to the default config that ships with a
fresh install:

- HellionThemeWindowOpacity 0.92 -> 0.5 so a fresh install lands at
  the more glass-like default the maintainer uses daily
- Use24HourClock false -> true to match a German / European locale.
  Works correctly thanks to the v0.5.1 strict-format fix that uses
  CultureInfo.InvariantCulture instead of the host culture
- HellionParty preset Channel: InputChannel.Party -> null. Auto-
  routing /party into a tab that also collects /alliance and /pvpteam
  surprises the user when they wanted to type into the other ones;
  the tab stays as a read surface

LoadPreviousSession and FilterIncludePreviousSessions stay false to
keep the privacy-strict 'every session starts fresh' line. The
maintainer's personal settings flip them on, but that's an
opt-in choice, not a default we should ship to every fresh install.
RetentionEnabled also stays false for the same opt-in reason.
2026-05-02 23:24:22 +02:00
JonKazama-Hellion 8d6868aef6 chore(config): align defaults with maintainer's live config
Four defaults now match what a daily-driver Hellion install ends up at
anyway, so a fresh install does not feel like the wrong product:

- HellionThemeWindowOpacity 0.92 -> 0.5 (more glass-like)
- LoadPreviousSession + FilterIncludePreviousSessions false -> true
  (tabs pick up where they left off after a crash or restart). The
  privacy filter still gates what goes into the store; loading what
  is already in there is not an additional privacy cost.
- Use24HourClock false -> true (matches a German / European locale,
  works with the strict CultureInfo.InvariantCulture format from the
  v0.5.1 fix).

RetentionEnabled stays at false because that one is a documented
opt-in privacy line, not a UX default. The persistent retention sweep
should require an explicit user gesture even though my own install
has it on.
2026-05-02 23:21:20 +02:00
JonKazama-Hellion 6e8fcc8cc3 merge: codeql manual-build workflow 2026-05-02 23:17:39 +02:00
JonKazama-Hellion 57670ffc76 ci(codeql): replace default setup with manual-build workflow
The default GitHub-managed CodeQL setup builds C# without the Dalamud
assemblies (they live in user AppData, not in the repo or in NuGet),
so call-target resolution sits at 64% and the analysis tile reports
'Low C# analysis quality'. This workflow runs the same Dalamud staging
download we use for the regular build before the CodeQL build step,
which gives the analyser a fully-resolved compilation and pushes both
quality metrics above the 85% thresholds.

Two jobs:

- analyze-csharp on windows-latest with build-mode: manual and the
  security-extended query suite, so we get the full SQL-injection,
  path-traversal and crypto-misuse rule set on a clean compilation
- analyze-actions on ubuntu-latest with build-mode: none, scans the
  workflow files in .github for action-injection patterns

Schedule runs Mondays at 06:17 UTC (low-traffic window).

The repo's CodeQL default setup needs to be switched to advanced in
Settings -> Code security before this workflow takes over, otherwise
both run in parallel and we waste runner minutes.
2026-05-02 23:15:20 +02:00
JonKazama-Hellion 2144eedd76 fix(autotell): exclude live tell from history preload
The live tell that triggers an Auto-Tell-Tab spawn is already in the
message store by the time MessageProcessed fires, because
MessageManager calls Store.UpsertMessage on line 266 before invoking
the event on line 277. PreloadHistory therefore picked up the live
tell as the youngest historic message and the separator landed below
it instead of above.

Pass the live message id through SpawnTempTab into PreloadHistory and
filter it out of the result. Pull one extra row so a successful
exclude does not cost the user a preload-budget slot.
2026-05-02 23:11:20 +02:00
JonKazama-Hellion 43daef83de merge: readme status badges 2026-05-02 23:05:24 +02:00
dependabot[bot] 4a9ad426e7 chore(deps): Bump Microsoft.Data.Sqlite from 9.0.0 to 10.0.7 (#5)
---
updated-dependencies:
- dependency-name: Microsoft.Data.Sqlite
  dependency-version: 10.0.7
  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-02 23:03:53 +02:00
dependabot[bot] 13beda3a8d chore(actions): bump softprops/action-gh-release from 2 to 3 (#3)
Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  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-02 23:03:49 +02:00
dependabot[bot] 18c05af4db chore(actions): bump actions/upload-artifact from 4 to 7 (#2)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  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-02 23:03:46 +02:00
dependabot[bot] df6e1e1cbd chore(actions): bump actions/checkout from 4 to 6 (#1)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  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-02 23:03:43 +02:00
JonKazama-Hellion 01b1a14511 docs(readme): add status badges above the title
Seven badges covering build status, CodeQL scanning, license, latest
release, Dalamud API level, .NET version and FFXIV expansion. Quick
visual indicator that the plugin is healthy and which tooling
generation it targets, plus shortcut links to the workflow runs and
security findings.
2026-05-02 23:00:13 +02:00
JonKazama-Hellion b6af8d559c merge: license detection fix and github workflows 2026-05-02 22:53:11 +02:00
JonKazama-Hellion 22dbfc2e24 chore(repo): fix license detection and add github workflows
LICENSE now starts with the EUPL-1.2 standard header so github-linguist
detects the licence correctly in the repo header. The dual-copyright
block (upstream ChatTwo authors plus Hellion Online Media) moves into a
new COPYRIGHT file referenced from the README. NOTICE.md and
UPSTREAM_SYNC.md stay as-is.

New files under .github:

- workflows/build.yml: validates every push to main and every PR
  against the current Dalamud staging branch on a Windows runner
- workflows/release.yml: builds Release on every v* tag, locates the
  DalamudPackager latest.zip and attaches it to the matching GitHub
  Release via softprops/action-gh-release
- dependabot.yml: weekly NuGet sweeps and monthly GitHub Actions
  sweeps with conventional-commit prefixes, grouped patch and minor
  PRs to cut review noise
- ISSUE_TEMPLATE/bug_report.yml + feature_request.yml + config.yml:
  structured intake that pushes security reports through the private
  advisory flow and routes upstream-only issues to ChatTwo
- SECURITY.md: documents the vulnerability reporting channels, scope,
  and target disclosure window

The release workflow replaces the previous manual upload step. Tag a
release and the ZIP shows up on the release page automatically.
2026-05-02 22:50:06 +02:00
JonKazama-Hellion 2f3b01732c merge: untrack test project from public repo 2026-05-02 22:09:34 +02:00
JonKazama-Hellion 88803382dd chore(repo): untrack ChatTwo.Tests, matches upstream layout
Drop the test project from version control. Upstream Chat 2 also
keeps ChatTwo.Tests outside the public repo, and the test sources
need a Dalamud assembly bundle that only resolves on a configured
Windows dev box anyway. The files stay on disk for local runs but
no longer ship with the source.
2026-05-02 22:08:47 +02:00
JonKazama-Hellion 74c51163c7 merge: license, notice and upstream sync docs 2026-05-02 22:06:53 +02:00
JonKazama-Hellion 877ff4ba18 chore(license): remove duplicate upstream LICENCE file
The LICENSE file added in ad2feb5 carries the dual-copyright block
(upstream ChatTwo authors plus Hellion Online Media) and is the one
the README now points at. The original Upstream-only LICENCE was a
verbatim copy of the EUPL text without our Hellion attribution and
became redundant the moment LICENSE landed. GitHub also prefers the
LICENSE filename for its license-detection in the repo header.
2026-05-02 21:53:06 +02:00
JonKazama-Hellion ad2feb5a27 chore(license): add LICENSE, NOTICE and upstream sync docs
Three new top-level files plus README update in preparation for
leaving the GitHub fork network:

- LICENSE: full EUPL-1.2 text plus dual copyright notice (upstream
  ChatTwo authors and Hellion Online Media). README previously
  pointed at a non-existent LICENCE file, fixing that compliance
  gap was overdue regardless of the fork-network decision.
- NOTICE.md: acknowledgements addressed directly to Infi and Anna,
  honest framing of why the fork exists alongside upstream rather
  than trying to displace it, plus maintainer contact channels for
  attribution or takedown questions.
- UPSTREAM_SYNC.md: documents the manual cherry-pick workflow with
  -x authorship preservation, the conflict-handling policy, and
  what we will and will not pull from upstream. Replaces the
  GitHub-Fork sync UI we will lose after detaching.
- README.md: version bump to 0.5.1, fork-network detach note, link
  to NOTICE.md and LICENSE, fixed the LICENCE / LICENSE typo.
2026-05-02 21:51:33 +02:00
JonKazama-Hellion 46b63ffdd1 merge: Hellion Chat 0.5.1 — Backlog Sweep 2026-05-02 21:34:50 +02:00
JonKazama-Hellion 4ba5004322 chore(release): bump version to 0.5.1
Hardening and polish patch. Eight backlog items from the v0.5.0
codebase review. No new features, no migration.
2026-05-02 21:27:10 +02:00
JonKazama-Hellion 3584c94523 docs(db): explain why pragma statements stay interpolated
Both PRAGMA call sites take values that SQLite does not accept as
bound parameters. ColumnExists takes a hardcoded table name, the
migration call takes a compile-time int from the version sequence.
Comments now state both facts so future readers don't try to wedge a
defensive whitelist into a path that cannot be reached from anywhere
user-controlled.
2026-05-02 21:25:40 +02:00
JonKazama-Hellion 303729f3d3 refactor(db): parameterise DeleteByRetentionPolicy SQL clauses
Per-channel WHERE tuples and the catch-all default-clause now bind
ChatType and cutoff via named parameters instead of being inlined as
literals. Combines BindIntList for the explicit-types exclusion with
explicit AddWithValue for each (type, cutoff) tuple. Behavioural diff
against v0.5.0: none — same retention windows, same cutoff math, just
parameterised.
2026-05-02 21:25:05 +02:00
JonKazama-Hellion 12085ff1e2 refactor(db): parameterise IN-clause SQL via BindIntList
Migrate CleanupRetainOnly, StreamForExport, CountDateRange, GetDateRange
and GetPagedDateRange from interpolated IN lists onto BindIntList.
Eliminates the string-interpolation pattern for SQL value lists in the
IN-clause sites. Behavioural diff against v0.5.0: none — same enum/byte
values, just bound under named parameters instead of inlined.
2026-05-02 21:24:36 +02:00
JonKazama-Hellion e4593a0fda refactor(db): add BindIntList helper for parameterised IN-clauses
Centralised builder for dynamic IN-clauses that binds each value as a
named parameter and returns the comma-joined placeholder string. Used
by the upcoming MessageStore migrations away from string-interpolated
SQL.
2026-05-02 21:23:17 +02:00
JonKazama-Hellion 3fc42963ae feat(autotell): dim selection background of greeted tabs
The text-disabled colour alone made greeted tabs visually weak in the
sidebar. Push dimmed Header and HeaderHovered values alongside the
existing Text push so the selected and hovered states match the
greeted state too. Idle state stays untouched because ImGui Selectable
has no idle background slot.
2026-05-02 21:22:36 +02:00
JonKazama-Hellion 7c52e890e6 feat(privacy): mark cleanup preview as stale when whitelist changes
The preview block caches the deletion estimate from the last refresh.
When the user toggles whitelist channels afterwards the cached number
no longer reflects the current selection. Snapshot the whitelist on
refresh and detect drift on every frame; on drift, grey out the counts
and surface a stale hint plus an emphasised refresh button. Sits
alongside the existing Cleanup_Help_SavedNote, which warns about a
different mismatch (mutable vs saved) and stays as-is.
2026-05-02 21:22:12 +02:00
JonKazama-Hellion 4d977d5118 fix(fonts): marshal font chooser results onto the framework thread
Three FontChooser ContinueWith handlers wrote Mutable.* directly from
the threadpool. Wrap the result-write in Plugin.Framework.Run so the
mutation lands on the same thread that owns the rest of the UI state.
Matches the marshalling pattern already used by Database.cs and
Privacy.cs background work.
2026-05-02 21:20:49 +02:00
JonKazama-Hellion ddd72a878e refactor(emotes): drop async void on LoadData
async void Task.Run() is a no-op for awaiting purposes since the void
returns immediately. Switch LoadData to async Task and have the two
callers fire-and-forget the task directly. Exceptions still go through
the existing try/catch inside LoadData.
2026-05-02 21:20:16 +02:00
JonKazama-Hellion 66450dd518 refactor(i18n): pull tabs and database tab names into hellionstrings
Both tab classes were the last two settings tabs still pulling their
display name from the upstream Language resource bundle. Move them
into HellionStrings so all eight settings tabs share one i18n source.
The unused Language.Options_*_Tab keys stay around for backwards
compat with cherry-picked upstream tabs.
2026-05-02 21:19:40 +02:00
JonKazama-Hellion 7de28ef9b2 refactor(settings): align performance section with helpmarker pattern
Drop the inline wall-of-text description in favour of the standard
HelpMarker tooltip used across the rest of the v0.5.0 settings UX.
Visual consistency for the General tab.
2026-05-02 21:18:30 +02:00
JonKazama-Hellion da3c1f6832 fix(emotes): mark required properties to silence CS8618
Mark Emote.Id, Top100.Id, Top100.Code and Top100.ImageType as required
so the JSON deserializer enforces the contract instead of relying on
default-null semantics. Removes the four CS8618 warnings the build has
been carrying since v0.4.0.
2026-05-02 21:17:57 +02:00
JonKazama-Hellion e66ae1f5b4 merge: Hellion Chat 0.5.0 — Settings UX Polish
Twelve organic settings tabs collapsed into eight themed ones (General,
Appearance, Window, Chat, Tabs, Privacy, Database, Information). Wipe
migration v9→v10 with HellionChat.json.pre-v10-backup safety net.
Default tab layout now spawns six themed tabs out of the box (General,
System, Free Company, Party, Beginner when Novice Network is on,
Linkshell, Tell Exclusive). HelpMarker pattern across every section,
disabled tooltips remain visible. Pre-release polish from full
codebase review covered race-conditions, EmoteCache retry, Allman
bracing, and dead i18n keys.
2026-05-02 18:42:50 +02:00
JonKazama-Hellion 281a1e172f feat(tabs): add dedicated System tab to default layout
Split the technical/notification streams (System, Error, Echo, Debug,
NPC announcements, login/logout, retainer sales, gathering system,
glamour notifications, sign messages, alarms, orchestrion, message
book, random number, progress) out of the General tab into their own
System tab. General now shows player conversation plus the active
gameplay events (loot rolls, crafting, gathering, NPC dialogue, party
finder pings) without burying chat under technical chatter.
2026-05-02 18:28:29 +02:00
JonKazama-Hellion 45a5035426 refactor(tabs): align General preset with maintainer's live config
Drop the channels that already live in dedicated themed tabs (Tells,
emotes, Novice Network, FC and PvP announcements, Sign and Glamour
notifications) so the General tab is the public-chat catch-all instead
of a duplicate of every themed tab. NpcDialogue moves in because the
maintainer reads it alongside system messages.
2026-05-02 18:25:59 +02:00
JonKazama-Hellion e1931fc7d2 feat(tabs): seed default tab layout on first run and v10 wipe
Spawn six themed tabs out of the box instead of one General catch-all:
General (everything), Free Company (FC chat plus FC announcements and
login/logout), Party (Party, CrossParty, Alliance, PvP team plus loot
rolls), Beginner (Novice Network only when ShowNoviceNetwork is on),
Linkshell (all eight regular and cross-world linkshells together) and
Tell Exclusive (TellIncoming/TellOutgoing as a safety-net catch-all in
case Auto-Tell-Tabs misses one).

Tab names live in HellionStrings (EN/DE). The Tabs settings tab gains a
help-text hint above the list recommending one tab per linkshell when
the user is in multiple, since a single combined Linkshell tab gets
noisy fast for active users.
2026-05-02 18:13:15 +02:00
JonKazama-Hellion 2201478a54 fix(v0.5.0): defaults and coupling issues from first walkthrough
- Configuration.cs: ShowTitleBar defaults to true so a fresh install
  shows the window header instead of leaving the user without a drag
  handle and hide button
- Configuration.cs: MaxLinesToRender default drops from 10000 to 5000
  to match the slider's intended ceiling and the previous user-tuned
  baseline
- ChatLogWindow.cs: 24h-clock checkbox now actually flips the format.
  The Bestand path passed null culture which on a German system
  locale always rendered 24h regardless of the toggle
- Appearance.cs + ChatLogWindow.cs + Popout.cs: when Hellion theme is
  enabled the global theme opacity drives the chat-window BgAlpha and
  the legacy WindowAlpha slider is disabled, so the two opacity
  controls no longer fight each other
- Appearance.cs: ticking UseHellionFont now flips FontsEnabled off so
  the two mutually-exclusive font stacks no longer appear active at
  the same time
2026-05-02 18:00:04 +02:00
JonKazama-Hellion 50963ccf1b fix(v0.5.0): pre-release polish from full-codebase review
- Plugin.cs: mark RetentionSweepRunning volatile so the ImGui thread
  reads the latest value without a stale register-cached copy
- EmoteCache.cs: reset State to Unloaded on exception so a later
  trigger can retry instead of being blocked by the early-out
- Settings.cs: switch the SaveAndClose / Discard buttons to Allman
  bracing for consistency with the rest of the file, and include the
  ItemSpacing in the Ko-fi-button right-edge calculation
- Privacy.cs: add a saved-policy hint above the manual retention
  Ctrl+Shift button so the existing Cleanup wording pattern is
  matched here too
- HellionStrings: drop seven unreferenced keys (Theme_Heading,
  Migration_Notification_*, Migration_Webinterface_Removed_*,
  AutoTellTabs_Migration_*) and their EN/DE values, add the new
  Retention_Help_SavedNote string
2026-05-02 17:50:32 +02:00
JonKazama-Hellion fde85e6d69 chore(release): bump version to 0.5.0
Settings UX polish release. Twelve organic settings tabs collapsed
into eight themed ones, theme/font controls moved to Appearance,
About and Changelog merged into Information. Configuration migrates
from v9 to v10 as a wipe with a backup file written next to the live
config; chat history and tabs survive.
2026-05-02 17:37:46 +02:00
JonKazama-Hellion c22b169b73 chore(settings): remove legacy settings tab files
The nine legacy tab implementations were superseded by the new eight
themed tabs. Removing them now that the consolidated structure is in
place keeps SettingsTabs/ aligned with what actually ships.
2026-05-02 17:36:31 +02:00
JonKazama-Hellion 6839ccaf34 feat(settings): build information tab from about and changelog
Three collapsible sections: version info (author, discord handle,
version, issue tracker), about HellionChat (maintainer, mission, build
lineage, license, SE notice, localisation, translator list), and the
changelog (auto-print toggle plus the manifest changelog renderer).
2026-05-02 17:36:07 +02:00
JonKazama-Hellion fa108c2271 refactor(settings): align tabs tab plugin reference to property style
Match the property-style reference used across the other settings tabs
so the field/property mix is gone.
2026-05-02 17:34:24 +02:00
JonKazama-Hellion 395a0d7c98 refactor(settings): polish database tab section structure
Drop the redundant inner Advanced TreeNode in the Maintenance section,
flatten the duplicated indent in the Overview section, and rename the
section headings so they reflect their content (Overview shows
metadata, Maintenance hosts the shift-gated tooling).
2026-05-02 17:33:51 +02:00
JonKazama-Hellion b76bfb3cfc refactor(settings-refactor): regroup Database tab into storage, viewer, stats tree nodes 2026-05-02 17:22:26 +02:00
JonKazama-Hellion 0512e4729c refactor(settings-refactor): remove theme settings from Privacy tab and tighten tree structure 2026-05-02 17:14:40 +02:00
JonKazama-Hellion 654f24c609 docs(settings-refactor): add Chat tab header explaining emote block migration 2026-05-02 17:09:50 +02:00
JonKazama-Hellion 0e2a14197c feat(settings-refactor): populate Chat tab with auto-tell-tabs, behaviour, preview, emotes 2026-05-02 17:04:25 +02:00
JonKazama-Hellion 52e163a472 fix(settings-refactor): show HelpMarker tooltip even when item is disabled 2026-05-02 17:00:25 +02:00
JonKazama-Hellion e086afe2a8 feat(settings-refactor): populate Window tab with hide, inactivity, frame, tooltips 2026-05-02 16:54:23 +02:00
JonKazama-Hellion c97ce7543b feat(settings-refactor): populate Appearance tab with theme, fonts, colours, timestamps 2026-05-02 16:44:50 +02:00
JonKazama-Hellion cca4571470 feat(settings-refactor): populate General tab with input, audio, performance, language sections 2026-05-02 16:36:52 +02:00
JonKazama-Hellion 444d7f8e2e style(settings-refactor): use property-style for Plugin reference in tab stubs 2026-05-02 16:31:42 +02:00
JonKazama-Hellion 71ae95d79c feat(settings-refactor): wire new 8-tab structure with stubs 2026-05-02 16:26:22 +02:00
JonKazama-Hellion 9a38f7f094 chore: rewrite AI disclosure in a more human, less legal-watertight tone 2026-05-02 16:22:48 +02:00
JonKazama-Hellion c33e519bb9 fix(settings-refactor): use pluginConfigs root for backup path 2026-05-02 16:03:51 +02:00
JonKazama-Hellion 14e585ef63 feat(settings-refactor): bump configuration version to 10 with wipe migration 2026-05-02 15:54:35 +02:00
202 changed files with 12298 additions and 4537 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
+13
View File
@@ -0,0 +1,13 @@
# HellionChat is a hobby project and does not solicit funding.
#
# If you want to support the work that made HellionChat possible,
# please consider supporting the upstream Chat 2 maintainers:
#
# Infiziert90 (Infi): https://ko-fi.com/infiii
# Anna Clemens: https://ko-fi.com/lojewalo
#
# Both Ko-fi pages are also linked in the plugin's settings panel.
# No platforms enabled — keep this file present so GitHub recognises
# the project as having considered funding without showing a Sponsor
# button on the repository page.
+73
View File
@@ -0,0 +1,73 @@
name: Bug report
description: Something in HellionChat is broken or behaves wrong
labels:
- bug
body:
- type: markdown
attributes:
value: |
Thanks for reporting. Please fill in the fields below so I can
reproduce the issue. If this is a security issue, stop here and
use the [private vulnerability advisory](https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new)
instead.
- type: input
id: version
attributes:
label: HellionChat version
description: From Settings → Information → Version
placeholder: "0.5.4"
validations:
required: true
- type: dropdown
id: platform
attributes:
label: Platform
options:
- Windows (XIVLauncher)
- Linux (XIVLauncher Core)
- macOS (XIVLauncher Core / wine)
- Other
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What happened
description: Plain description, no log dumps yet
validations:
required: true
- type: textarea
id: expected
attributes:
label: What you expected
validations:
required: true
- type: textarea
id: steps
attributes:
label: How to reproduce
description: Step-by-step from "open settings" or "log in" through to the broken behaviour
validations:
required: true
- type: textarea
id: log
attributes:
label: Relevant /xllog excerpt
description: Filter for "HellionChat" if the log is huge
render: text
- type: checkboxes
id: confirm
attributes:
label: Pre-flight
options:
- label: I am running the latest version of HellionChat
required: true
- label: I have searched existing issues for duplicates
required: true
+14
View File
@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: Security vulnerability
url: https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
about: Do not open a public issue for security problems. Use the private advisory instead.
- name: Upstream Chat 2 issue
url: https://github.com/Infiziert90/ChatTwo/issues
about: If the issue exists in upstream Chat 2 too, please report it there so the original maintainers see it as well.
- name: Discord
url: https://discord.com/users/j.j_kazama
about: Quick questions, casual feedback. Bug reports still go through the issue tracker for tracking.
@@ -0,0 +1,55 @@
name: Feature request
description: Suggest a feature or enhancement for HellionChat
labels:
- enhancement
body:
- type: markdown
attributes:
value: |
Thanks for the suggestion. HellionChat focuses on privacy by
default and a small, well-scoped feature set. Suggestions that
align with that scope are easier to accept than ones that pull
the plugin toward "do everything".
- type: textarea
id: problem
attributes:
label: What problem are you trying to solve
description: The user-side problem, not the proposed solution yet
validations:
required: true
- type: textarea
id: solution
attributes:
label: What you would like HellionChat to do
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives you have considered
description: Other plugins, manual workarounds, settings combinations
- type: dropdown
id: scope
attributes:
label: Scope estimate from your side
options:
- "Small (one tab, one toggle, one filter)"
- "Medium (a settings section, persistent state, one new file)"
- "Large (architectural, touches the message pipeline or the database)"
- "I don't know"
validations:
required: true
- type: checkboxes
id: confirm
attributes:
label: Pre-flight
options:
- label: I have searched existing issues for similar requests
required: true
- label: I understand HellionChat is a privacy-focused fork and not a feature parity tool with upstream Chat 2
required: true
+72
View File
@@ -0,0 +1,72 @@
<!--
Thanks for contributing to HellionChat. Please fill in the sections
below so the review goes quickly. Delete sections that genuinely do
not apply, but do not delete the whole template.
If this is a security fix, stop here and use a private security
advisory instead:
https://github.com/JonKazama-Hellion/HellionChat/security/advisories/new
-->
## Summary
<!-- One or two sentences. What does this PR change and why. -->
## Type of change
<!-- Tick all that apply. -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds behaviour)
- [ ] Breaking change (config migration, removed feature, or behaviour
change that user-visible defaults rely on)
- [ ] Documentation only
- [ ] Translation update
- [ ] Build, CI or tooling change
- [ ] Upstream cherry-pick from Chat 2
## Linked issue
<!-- e.g. "Closes #42" or "Refs #42". For trivial typo fixes, "n/a". -->
## How I tested this
<!--
- Built locally with `dotnet build -c Release`
- Ran `dotnet test`
- Loaded the plugin in-game on Windows / Linux / macOS via XIVLauncher
- Specific scenarios I exercised in-game
-->
## User-visible changes
<!--
Anything the end user will notice. New settings, changed defaults,
new commands, new translations, removed behaviour. If none, write
"none".
-->
## Compatibility notes
<!--
- Does this require a configuration migration? If yes, which version
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 docs/UPSTREAM_SYNC.md.
-->
## Checklist
- [ ] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and
[CODE_OF_CONDUCT.md](../CODE_OF_CONDUCT.md).
- [ ] My change matches the existing code style (`.editorconfig`).
- [ ] I added or updated tests where the existing test infrastructure
made that practical, or I have explained why tests are not
applicable.
- [ ] 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](../docs/AI_DISCLOSURE.md)).
- [ ] I confirm my contribution is released under the
[EUPL-1.2](../LICENSE).
+42
View File
@@ -0,0 +1,42 @@
version: 2
updates:
# NuGet package updates for the plugin project. Weekly cadence keeps the
# noise down while still catching transitive security advisories within
# a few days of disclosure.
- package-ecosystem: nuget
directory: /HellionChat
schedule:
interval: weekly
day: monday
time: "07:00"
timezone: Europe/Berlin
open-pull-requests-limit: 5
labels:
- dependencies
- nuget
commit-message:
prefix: "chore(deps)"
groups:
patches:
update-types:
- patch
minor:
update-types:
- minor
# GitHub Actions versions in .github/workflows. Lower cadence because
# Action releases ship less frequently and are usually safe to defer
# for a month.
- package-ecosystem: github-actions
directory: /
schedule:
interval: monthly
time: "07:00"
timezone: Europe/Berlin
open-pull-requests-limit: 3
labels:
- dependencies
- github-actions
commit-message:
prefix: "chore(actions)"
+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`
+16
View File
@@ -0,0 +1,16 @@
---
subtitle: "Layout Refresh"
versionsnatur: "Major-UI-Cycle"
---
- Sidebar im neuen Look: fix 44 px breit, nur Icons, Tab-Name als Tooltip beim Hover, vertikale Akzent-Pill markiert den aktiven Tab
- Top-Tabs bekommen eine Akzent-Underline statt Background-Fill am aktiven Tab
- Pro Tab eigenes Icon wählbar in Einstellungen → Tabs (FontAwesome-Pool)
- Auto-Tell-Tabs sind jetzt visuell unterscheidbar: jeder Tell-Partner bekommt ein eigenes Icon (envelope/star/heart/bell/bookmark/flag/fire) plus eigene Farbe aus 12-Farb-Palette — 84 Kombinationen, gleicher Partner ergibt konsistent dieselbe
- Pulsierender roter Dot oben rechts am Sidebar-Icon zeigt ungelesene Nachrichten an. Sanft, 2-Sekunden-Cycle, deaktivierbar über `Configuration.ReduceMotion` (UI-Toggle in v1.3.0)
- Bottom-Status-Bar (22 px) mit fünf Live-Slots: aktiver Channel + Color-Dot, Privacy-Badge, Tab/Message-Counter, Auto-Tell-Counter, Plugin-Version. Update 1×/Sek
- Card-Rows als Default-Message-Render: Sender-Header in Channel-Farbe, Body neue Zeile, dezenter Trenner. `Compact Density`-Toggle in Aussehen schaltet zurück auf den Einzeiler
- Bug-Fix: Settings speichern löscht den Chat-Verlauf nicht mehr. Refilter läuft jetzt nur wenn Filter-relevante Settings geändert wurden — Cosmetic-Änderungen lassen den Chat unverändert. Persistente und Auto-Tell-Tabs überleben beide
- Bug-Fix: Hellion-Schrift (Exo 2) blockt die Schriftgröße nicht mehr — 4K-User können hochskalieren
- Migration v14 → v15: alte Theme-Felder entfernt, alle anderen Settings bleiben
Animation-Polish (Lerps, Theme-Crossfade, Quick-Picker) folgt in v1.3.0.
+16
View File
@@ -0,0 +1,16 @@
---
subtitle: "Settings Cleanup"
versionsnatur: "UX-Polish-Cycle"
---
- Settings-Übersicht thematisch re-sortiert: zusammenhängende Optionen wohnen jetzt zusammen, jede Card hat einen kurzen Untertitel — kein Raten mehr wo eine Setting steckt
- Drei neue Cards: **Theme & Layout** (Theme-Picker, Fenster-Style, Zeitstempel-Style), **Schriften & Farben** (Schriftart, Schriftgröße, Chat-Farben pro Channel), **Daten-Verwaltung** (Aufbewahrung, Cleanup, Export, DB-Viewer, Advanced-Tools — vorher zwischen Datenschutz und Datenbank verteilt)
- Datenschutz fokussiert sich jetzt auf eine Aufgabe: den Privacy-Filter
- Der Auto-Tell-Tabs-History-Preload-Slider ist von Datenschutz nach Chat → Auto-Tell-Tabs umgezogen
- KeybindMode wohnt jetzt unter Allgemein → Eingabe statt unter Sprache
- Vier tote Schema-Felder entfernt (alle obsolet seit der Theme-Engine in v1.1.0): `Stilüberschreiben`-Toggle, `Stilname`-Auswahl, alter `WindowAlpha`-Slider, ungenutztes `ShowThemeQuickPicker`
- Migration v15 → v16: alter `WindowAlpha`-Wert wird automatisch nach `Theme & Layout → Fenster-Style → Fenster-Transparenz` gemappt (nur wenn der Slider noch auf Default 0.85 stand, sonst gewinnt der User-Wert). Backup der Pre-v16-Config liegt unter `pluginConfigs/HellionChat.json.pre-v16-backup`. User die `Stilüberschreiben` aktiv hatten sehen einen einmaligen Hinweis-Toast
- UX-Default-Bumps für Bestand-User mit Default-Werten: Card-Rows-Layout zurück auf Single-Line, NG+ standardmäßig hidden, gleiche Zeitstempel werden zusammengefasst, MaxLinesToRender auf konservativere 2500
- Frische Installs starten mit dem Hellion-Brand-Chat-Color-Preset out-of-the-box (der First-Run-Wizard hat keine Preset-Wahl)
- Hinweis zum Window-Transparenz-Slider in der Beschreibung: Dalamud's per-Window-Hamburger-Menü (oben rechts in der Titelleiste) bietet eigene Overrides für Deckkraft, Hintergrund-Blur, Anpinnen und Durchklick — die haben Vorrang über unseren Slider für das jeweilige Fenster
Pure UX-Polish, keine neuen Features. Nächster Cycle (v1.3.0): Animation-Polish (Lerps, Theme-Crossfade, Quick-Picker) wie ursprünglich geplant.
+13
View File
@@ -0,0 +1,13 @@
---
subtitle: "Theme Expansion"
versionsnatur: "Theme-Pack-Patch"
---
- Vier neue Built-in-Themes verlängern die Auswahl im Picker — keine Engine-Änderung, keine Settings angefasst, einfach mehr Farboptionen
- **Night Blue** — Royal Blue auf tiefem Marineblau. Kühles Tech-Dashboard-Mood, bewusst neutral gehalten damit es sich nicht mit den Brand-Themes beißt
- **Indigo Violet** — Royal Violet auf Deep Indigo mit Türkis-Mint-Counter für Aurora-Glitter-Stimmung. Schwester von Event Horizon, aber dunkler und dichter; der Türkis-Akzent hält die beiden klar auseinander
- **Forge Merchantman** — Patina-Bronze auf Workshop-Slate mit warmem Bernstein-Counter. Hellion Forge bekommt ein eigenes Theme im Plugin selbst — Schwester von Hellion Arctic, aber grüner und wärmer statt kaltem Cyan
- **Hellion Spectrum** — Farbenblind-sichere Channel-Farben (Deuteranopie/Protanopie) auf Basis der Wong/Okabe-Ito-Palette. Channel-Identität bleibt erhalten (Tell pink, Yell gelb, Shout orange, Party blau, FC grün); die Töne sind so gewählt dass jeder Channel auch unter Rot-Grün-Schwäche klar trennbar bleibt. Deckt rund 99 % aller CVD-Fälle ab
- Kein Schema-Bump, keine Migration. Das Default-Theme bleibt **Hellion Arctic**, eigene Custom-Themes laufen unverändert weiter
- Theme-Katalog wächst damit von fünf auf neun Built-ins
Reines Theme-Pack zwischen v1.2.1 und dem nächsten Polish-Cycle. Eine Tritan-Variante (Spectrum für Blau-Gelb-Schwäche) kann später nachgeliefert werden, falls Bedarf kommt.
+26
View File
@@ -0,0 +1,26 @@
---
## How to install
This release is distributed via the HellionChat custom repository, not the
Dalamud main plugin repo. To install:
1. In XIVLauncher: **Settings → Experimental → Custom Plugin Repositories**
2. Add the URL:
`https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/repo.json`
3. Enable, save, then `/xlplugins` → search **Hellion Chat** → install
## Project documents
- [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/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
## Licence
[EUPL-1.2](https://github.com/JonKazama-Hellion/HellionChat/blob/main/LICENSE).
Based on [Chat 2](https://github.com/Infiziert90/ChatTwo) by Infi and Anna,
also EUPL-1.2.
+56
View File
@@ -0,0 +1,56 @@
name: Build
# Verifies that every push to main and every PR still builds against the
# current Dalamud staging branch. Does not produce release artefacts; the
# release workflow handles that on tag.
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
# Minimum permissions for a build-only workflow: read the repo, nothing
# else. Closes the CodeQL "Workflow does not contain permissions" alert
# and matches the principle-of-least-privilege the security guide
# recommends for workflows that don't push or create releases.
permissions:
contents: read
jobs:
build:
name: Build (Release)
runs-on: windows-latest
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Download Dalamud staging
shell: pwsh
run: |
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
- name: Restore
run: dotnet restore HellionChat/HellionChat.csproj
- name: Build (Release)
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: HellionChat/bin/Release/**/HellionChat/**
if-no-files-found: warn
retention-days: 14
+93
View File
@@ -0,0 +1,93 @@
name: CodeQL
# Replaces the GitHub default-setup CodeQL scan. The default setup runs
# without resolving the Dalamud assemblies (they live in a user-AppData
# path) and reports "Low C# analysis quality" because call-target
# resolution sits at ~64%. This workflow downloads the Dalamud staging
# distribution before the build, runs a manual dotnet build, and then
# lets CodeQL analyse the fully-resolved compilation. Quality climbs
# back above the 85% thresholds.
#
# This workflow only consumes trusted inputs: the tag/branch ref via
# the standard checkout action, and the Dalamud distribution URL which
# is pinned to a goatcorp-controlled GitHub Pages target. No user-
# controlled event payload (issue title, PR body, commit message) flows
# into a run-step.
#
# Disable the default setup in the repo before this workflow lands:
# Settings -> Code security -> Code scanning -> "CodeQL analysis" tile
# -> Switch to advanced.
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '17 6 * * 1'
permissions:
actions: read
contents: read
security-events: write
jobs:
analyze-csharp:
name: Analyze (csharp)
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup .NET 10
uses: actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7 # v5.2.0
with:
dotnet-version: 10.0.x
- name: Download Dalamud staging
shell: pwsh
run: |
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
- name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: csharp
build-mode: manual
queries: security-extended
- name: Restore
run: dotnet restore HellionChat/HellionChat.csproj
- name: Build (Release)
run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: /language:csharp
analyze-actions:
name: Analyze (actions)
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Initialize CodeQL
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
languages: actions
build-mode: none
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
with:
category: /language:actions
+226
View File
@@ -0,0 +1,226 @@
name: Forge Announce
# Triggered when a vX.Y.Z tag is pushed. Reads .github/forge-posts/<tag>.md
# (Frontmatter + DE bullet body) and the matching English block from
# HellionChat/HellionChat.yaml, builds a Discord-Webhook embed and posts
# it to the Hellion Forge #changelog channel.
#
# Decoupled from release.yml: a fail here does not block the GitHub
# release, and a fail there does not block the announce. Spec lives in
# the Vault under "Hellion Chat Forge-Auto-Announce Spec".
#
# Security: the only user-controlled inputs that enter run-steps are the
# tag name and the frontmatter values from a repo-internal markdown file.
# Tag name is read via env: (TAG_NAME, $env:TAG_NAME) and validated against
# ^v\d+\.\d+\.\d+$ before any string interpolation. Frontmatter values are
# parsed by regex with explicit length caps. No webhook event payload data
# (issue titles, PR bodies, commit messages, etc.) flows into run-steps.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Existing tag to (re)post, e.g. v1.1.0'
required: true
type: string
permissions:
contents: read
jobs:
announce:
name: Post changelog to Hellion Forge
runs-on: ubuntu-latest
# The DISCORD_FORGE_WEBHOOK secret lives under Settings → Environments
# → Webhook (case-sensitive). Without this declaration the secret is
# not in scope for the job.
environment: Webhook
timeout-minutes: 5
steps:
# On push:tags github.ref points at the tag commit; on workflow_dispatch
# the user supplies the tag explicitly. Always check out that tag so
# the yaml + forge-posts file are read from the tagged tree, not main.
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.inputs.tag || github.ref }}
# Build embed-payload as a JSON file on disk. PowerShell-Core (pwsh)
# ships pre-installed on ubuntu-latest so we get the same scripting
# patterns release.yml uses on windows-latest. Tag is read via env: to
# treat it as a string variable rather than inline shell text, and
# validated against the semver regex before any interpolation.
- name: Build embed payload
id: build
shell: pwsh
env:
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
run: |
$tag = $env:TAG_NAME
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
throw "V1: Refusing to announce non-semver tag: $tag"
}
$version = $tag.Substring(1)
# ---------- Forge-Post-Datei lesen ----------
$forgePath = ".github/forge-posts/$tag.md"
if (-not (Test-Path $forgePath)) {
throw "V2: Forge-Post-Datei für $tag fehlt unter .github/forge-posts/. Datei vor dem Tag anlegen, dann Tag re-pushen oder workflow_dispatch."
}
$forgeRaw = Get-Content -Path $forgePath -Raw
# Frontmatter (--- … ---) am Datei-Anfang
if ($forgeRaw -notmatch '(?s)\A---\s*\r?\n(.*?)\r?\n---\s*\r?\n(.*)\z') {
throw "V3: Frontmatter (---) fehlt oder ist defekt in $forgePath"
}
$fmText = $matches[1]
$deBody = $matches[2].Trim()
$subtitle = $null
$versionsnatur = $null
foreach ($line in ($fmText -split "`r?`n")) {
if ($line -match '^subtitle:\s*"?([^"]*)"?\s*$') { $subtitle = $matches[1] }
if ($line -match '^versionsnatur:\s*"?([^"]*)"?\s*$') { $versionsnatur = $matches[1] }
}
if ([string]::IsNullOrWhiteSpace($subtitle)) { throw "V3: Frontmatter-Feld 'subtitle' fehlt in $forgePath" }
if ([string]::IsNullOrWhiteSpace($versionsnatur)) { throw "V3: Frontmatter-Feld 'versionsnatur' fehlt in $forgePath" }
if ($subtitle.Length -gt 60) { throw "V4: Frontmatter-Feld 'subtitle' überschreitet Limit ($($subtitle.Length) Char, max 60)" }
if ($versionsnatur.Length -gt 40) { throw "V4: Frontmatter-Feld 'versionsnatur' überschreitet Limit ($($versionsnatur.Length) Char, max 40)" }
if ([string]::IsNullOrWhiteSpace($deBody)) { throw "V3: DE-Body fehlt in $forgePath" }
# ---------- EN-Block aus HellionChat.yaml ziehen ----------
# 1:1 Pattern aus release.yml — gleicher Header-Marker, gleiches
# Trailer-Verhalten. Bei Drift die zwei Workflows synchron halten.
$yamlPath = "HellionChat/HellionChat.yaml"
$raw = Get-Content -Path $yamlPath -Raw
$marker = "changelog: |-"
$idx = $raw.IndexOf($marker)
if ($idx -lt 0) { throw "V5: changelog-Block nicht gefunden in $yamlPath" }
$afterMarker = $raw.Substring($idx + $marker.Length)
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
}) -join "`n"
$header = "**Hellion Chat $version"
$start = $changelogBody.IndexOf($header)
if ($start -lt 0) {
throw "V5: No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging."
}
$rest = $changelogBody.Substring($start)
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
$trailer = $rest.IndexOf("`n`n---")
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
$enBlock = $rest.Substring(0, $nextHdr).TrimEnd()
} elseif ($trailer -ge 0) {
$enBlock = $rest.Substring(0, $trailer).TrimEnd()
} else {
$enBlock = $rest.TrimEnd()
}
# ---------- Char-Cap-Check (5500 Total auf title + description + footer) ----------
$title = "Hellion Chat $version — $subtitle"
$description = "**Deutsch**`n`n$deBody`n`n**English**`n`n$enBlock"
$footerText = "Hellion Forge · $versionsnatur"
$totalChars = $title.Length + $description.Length + $footerText.Length
if ($totalChars -gt 5500) {
throw "V6: Total char count $totalChars exceeds 5500 limit. Major-Release detected — please post manually via Bot/Multi-Embed (see forge style §8). Forge-Auto-Announce stays off for this tag."
}
Write-Host "Char-Cap OK: $totalChars / 5500"
# ---------- Embed-Payload bauen ----------
$payload = [ordered]@{
username = "Forge Herald"
avatar_url = "https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/HellionChat/images/icon.png"
content = "<@&1500489631555260446>"
allowed_mentions = [ordered]@{
parse = @()
roles = @("1500489631555260446")
}
embeds = @(
[ordered]@{
title = $title
url = "https://github.com/JonKazama-Hellion/HellionChat/releases/tag/$tag"
color = 12730636
description = $description
footer = [ordered]@{ text = $footerText }
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
}
)
}
$payloadJson = $payload | ConvertTo-Json -Depth 8 -Compress
# Ausgabe-Datei ohne trailing newline für sauberes curl --data-binary @-
[System.IO.File]::WriteAllText("$PWD/embed-payload.json", $payloadJson, [System.Text.UTF8Encoding]::new($false))
Write-Host "Payload size: $($payloadJson.Length) chars"
Write-Host "Embed title: $title"
Write-Host "Embed footer: $footerText"
# POST to the Hellion Forge changelog webhook. curl from PowerShell-Core
# so we can pipe the payload via stdin (--data-binary @-) and keep
# secrets out of process arg lists. One retry on 5xx, hard fail on 4xx.
- name: POST to Hellion Forge webhook
shell: pwsh
env:
DISCORD_FORGE_WEBHOOK: ${{ secrets.DISCORD_FORGE_WEBHOOK }}
run: |
if ([string]::IsNullOrEmpty($env:DISCORD_FORGE_WEBHOOK)) {
throw "V7: DISCORD_FORGE_WEBHOOK secret is empty. Check Settings → Environments → Webhook."
}
$payloadFile = "$PWD/embed-payload.json"
if (-not (Test-Path $payloadFile)) {
throw "Embed payload file missing — previous step did not produce embed-payload.json"
}
$maxAttempts = 2
$attempt = 0
while ($attempt -lt $maxAttempts) {
$attempt++
Write-Host "POST attempt $attempt of $maxAttempts"
$tmpResp = "$PWD/.webhook-response"
$tmpHeaders = "$PWD/.webhook-headers"
# --silent suppresses progress; --show-error prints errors so
# the workflow log shows what happened. -w prints HTTP status
# to stdout for inspection. -o captures body for diagnosis,
# -D captures headers.
$rawStatus = Get-Content $payloadFile -Raw |
curl --silent --show-error `
--header 'Content-Type: application/json' `
--data-binary '@-' `
-D $tmpHeaders `
-o $tmpResp `
-w '%{http_code}' `
"$env:DISCORD_FORGE_WEBHOOK"
$status = [int]$rawStatus
Write-Host "HTTP status: $status"
if ($status -ge 200 -and $status -lt 300) {
Write-Host "Forge announce POST succeeded."
exit 0
}
$bodySnippet = ""
if (Test-Path $tmpResp) {
$bodySnippet = (Get-Content $tmpResp -Raw -ErrorAction SilentlyContinue)
if ($bodySnippet.Length -gt 500) { $bodySnippet = $bodySnippet.Substring(0, 500) + " …" }
}
if ($status -ge 400 -and $status -lt 500) {
# E2: 4xx is permanent — webhook revoked, channel deleted,
# payload malformed. No retry.
throw "E2: Discord-Webhook returned permanent $status. Body: $bodySnippet"
}
# E1: 5xx (or transport-level fail with status 0) — wait + retry once
if ($attempt -lt $maxAttempts) {
Write-Host "Transient $status — sleeping 30s before retry."
Start-Sleep -Seconds 30
} else {
throw "E1: Discord-Webhook returned transient $status after $maxAttempts attempts. Body: $bodySnippet"
}
}
+164
View File
@@ -0,0 +1,164 @@
name: Release
# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
# current Dalamud staging branch, locates the latest.zip produced by
# DalamudPackager and attaches it to the matching GitHub Release.
#
# User-controlled inputs touched by this workflow:
# - 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
# 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.
on:
push:
tags:
- 'v*'
# Manual recovery trigger. Use when a tag was pushed but the auto-run
# was missed or failed: `gh workflow run release.yml -f tag=v0.6.1`.
# The tag input is validated against the same semver regex as the
# auto-trigger before any string interpolation happens.
workflow_dispatch:
inputs:
tag:
description: 'Existing tag to (re)release, e.g. v0.6.1'
required: true
type: string
permissions:
contents: write
jobs:
release:
name: Build and attach release ZIP
runs-on: windows-latest
timeout-minutes: 20
steps:
# On push:tags, github.ref_name is the tag — checkout default works.
# On workflow_dispatch, ref defaults to the branch the action was
# invoked from; we need to explicitly check out the tag the user
# supplied so the build comes from the tagged commit, not main.
- name: Checkout
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.tag || github.ref }}
- name: Setup .NET 10
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Download Dalamud staging
shell: pwsh
run: |
$hooks = Join-Path $env:APPDATA "XIVLauncher\addon\Hooks\dev"
New-Item -ItemType Directory -Force -Path $hooks | Out-Null
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile dalamud.zip
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
- name: Build (Release)
run: dotnet build HellionChat/HellionChat.csproj --configuration Release
- name: Locate latest.zip
id: locate
shell: pwsh
run: |
$zip = Get-ChildItem -Path HellionChat\bin\Release -Recurse -Filter latest.zip | Select-Object -First 1
if (-not $zip)
{
throw "latest.zip not found under HellionChat\bin\Release"
}
Write-Host "Found: $($zip.FullName)"
"path=$($zip.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
# Build a release body from the matching changelog block in
# HellionChat.yaml plus a static install / docs footer. Fails the
# workflow if no block exists for the tagged version, which is the
# automated counterpart to the "yaml + repo.json + release body
# kept in sync" rule.
#
# GITHUB_REF_NAME is read via env: (not ${{ }} interpolation) so the
# tag value is treated as a PowerShell variable, not as inline shell
# text. The strict regex below rejects anything that is not a clean
# semver tag before it is used to build a string.
- name: Generate release body
shell: pwsh
env:
# workflow_dispatch carries the user-supplied tag in inputs.tag;
# push:tags carries it in github.ref_name. Either way the value
# is treated as a PowerShell variable (env-var pass), not as
# inline shell text, and validated against the semver regex
# below before any string interpolation.
TAG_NAME: ${{ github.event.inputs.tag || github.ref_name }}
run: |
$tag = $env:TAG_NAME
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
throw "Refusing to generate release body for non-semver tag: $tag"
}
$version = $tag.Substring(1)
$yamlPath = "HellionChat/HellionChat.yaml"
$raw = Get-Content -Path $yamlPath -Raw
$marker = "changelog: |-"
$idx = $raw.IndexOf($marker)
if ($idx -lt 0) { throw "changelog block not found in $yamlPath" }
# changelog: is the last top-level key in the manifest, so
# everything after the marker is the literal block. Strip the
# 2-space yaml indent from each line.
$afterMarker = $raw.Substring($idx + $marker.Length)
$changelogBody = (($afterMarker -split "`r?`n") | ForEach-Object {
if ($_ -match '^ ') { $_.Substring(2) } else { $_ }
}) -join "`n"
$header = "**Hellion Chat $version"
$start = $changelogBody.IndexOf($header)
if ($start -lt 0) {
throw "No changelog entry for version $version found in $yamlPath. Update the changelog block before tagging a release."
}
$rest = $changelogBody.Substring($start)
$nextHdr = $rest.IndexOf("`n`n**Hellion Chat ", 1)
$trailer = $rest.IndexOf("`n`n---")
if ($nextHdr -ge 0 -and ($trailer -lt 0 -or $nextHdr -lt $trailer)) {
$currentBlock = $rest.Substring(0, $nextHdr).TrimEnd()
} elseif ($trailer -ge 0) {
$currentBlock = $rest.Substring(0, $trailer).TrimEnd()
} else {
$currentBlock = $rest.TrimEnd()
}
# Static install / docs / licence footer is maintained as a
# separate file so the workflow YAML stays clean (no embedded
# heredoc that would have to be indented under the run-block).
$footerPath = ".github/release-footer.md"
if (-not (Test-Path $footerPath)) {
throw "Release footer template not found: $footerPath"
}
$footer = Get-Content -Path $footerPath -Raw
$body = $currentBlock + "`n" + $footer
$body | Out-File -FilePath release-body.md -Encoding utf8 -NoNewline
Write-Host "Generated release body for $tag :"
Write-Host "----------------------------------------"
Write-Host $body
Write-Host "----------------------------------------"
- name: Attach to GitHub release
uses: softprops/action-gh-release@v3
with:
# Explicit tag_name so the action targets the correct release in
# both push:tags (auto) and workflow_dispatch (manual recovery)
# modes. Without this, dispatch runs would default to the branch
# ref (main) and fail to find the release.
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
files: ${{ steps.locate.outputs.path }}
body_path: release-body.md
fail_on_unmatched_files: true
generate_release_notes: false
+7
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/
@@ -374,6 +378,9 @@ FodyWeavers.xsd
#Specs und Plan datein
/.superpowers/
#Test Datein
ChatTwo.Tests
TestResults
*.db-shm
*.db-wal
-42
View File
@@ -1,42 +0,0 @@
# AI assistance disclosure
Per the [Dalamud Plugin AI Usage Policy](https://github.com/goatcorp/DalamudPluginsD17/),
this fork uses AI assistance at the **Pair** level. Pair means the maintainer
plans the architecture, decides what gets built, reviews each change and
tests against the running game; Claude (Anthropic) helps explain Dalamud
APIs, suggests patterns, drafts code on request, and reviews approaches.
Neither side acts autonomously: nothing ships without the maintainer's
review, and Claude can't run the game.
The level varies by area and over time. Some commits are mostly hand-written
with the AI used as a sounding board, others lean more on Claude for an API
walkthrough or a code draft that the maintainer then reads, edits and
integrates. The maintainer's commitment is to be able to explain why every
piece of Hellion code is the way it is — not "I typed every character."
## What's where
Upstream Chat 2 (by Infi & Anna, EUPL-1.2) is the foundation and was not
produced with AI assistance. Hellion-specific code lives in
`ChatTwo/Privacy/`, `ChatTwo/Export/`, `Resources/HellionStrings*`,
`Ui/SettingsTabs/Privacy.cs`, `Ui/FirstRunWizard.cs`, `Ui/HellionStyle.cs`,
plus the Migrate3 recovery and plugin layout migration in `MessageStore.cs`
and `Plugin.cs`. These were developed with Pair-level assistance as
described above; the share of human vs. AI authorship varies file by file
and is expected to keep shifting toward more hand-written work as the
maintainer's plugin-dev experience grows.
## What AI is not used for
- **Visual assets.** Logos, icons, banners, screenshots are human-drawn or
taken from the running game.
- **German translations.** Written by the maintainer (native speaker).
## Tooling
- Claude (Anthropic) via Claude Code CLI as the main pair partner.
- Context7 / Microsoft Learn for current Dalamud and .NET documentation.
## Contact
Questions about this disclosure: <https://github.com/JonKazama-Hellion/HellionChat/issues>.
+133
View File
@@ -0,0 +1,133 @@
# Code of Conduct
## A Note on This Project
HellionChat is a one-person side project developed under Hellion Forge.
I maintain this in my spare time, which means replies can take a few
days. Please do not escalate just because a thread is quiet.
When in doubt, assume good intent. Contributors come from different
backgrounds, time zones and skill levels. A clarifying question is
almost always a better first move than an accusation.
Please also keep discussions on topic. This project is about a Dalamud
chat plugin. Off-topic arguments belong elsewhere.
---
## Our Pledge
We pledge to make our community welcoming, safe, and equitable for all.
We are committed to fostering an environment that respects and promotes
the dignity, rights, and contributions of all individuals, regardless
of characteristics including race, ethnicity, caste, color, age,
physical characteristics, neurodiversity, disability, sex or gender,
gender identity or expression, sexual orientation, language, philosophy
or religion, national or social origin, socio-economic position, level
of education, or other status. The same privileges of participation are
extended to everyone who participates in good faith and in accordance
with this Covenant.
## Encouraged Behaviors
While acknowledging differences in social norms, we all strive to meet
our community's expectations for positive behavior. We also understand
that our words and actions may be interpreted differently than we intend
based on culture, background, or native language.
With these considerations in mind, we agree to behave mindfully toward
each other and act in ways that center our shared values, including:
1. Respecting the **purpose of our community**, our activities, and our
ways of gathering.
2. Engaging **kindly and honestly** with others.
3. Respecting **different viewpoints** and experiences.
4. **Taking responsibility** for our actions and contributions.
5. Gracefully giving and accepting **constructive feedback**.
6. Committing to **repairing harm** when it occurs.
7. Behaving in other ways that promote and sustain the **well-being of
our community**.
## Restricted Behaviors
We agree to restrict the following behaviors in our community.
Instances, threats, and promotion of these behaviors are violations of
this Code of Conduct.
1. **Harassment.** Violating explicitly expressed boundaries or engaging
in unnecessary personal attention after any clear request to stop.
2. **Character attacks.** Making insulting, demeaning, or pejorative
comments directed at a community member or group of people.
3. **Stereotyping or discrimination.** Characterizing anyone's
personality or behavior on the basis of immutable identities or
traits.
4. **Sexualization.** Behaving in a way that would generally be
considered inappropriately intimate in the context or purpose of the
community.
5. **Violating confidentiality.** Sharing or acting on someone's
personal or private information without their permission.
6. **Endangerment.** Causing, encouraging, or threatening violence or
other harm toward any person or group.
7. Behaving in other ways that **threaten the well-being** of our
community.
### Other Restrictions
1. **Misleading identity.** Impersonating someone else for any reason,
or pretending to be someone else to evade enforcement actions.
2. **Failing to credit sources.** Not properly crediting the sources of
content you contribute.
3. **Promotional materials.** Sharing marketing or other commercial
content in a way that is outside the norms of the community.
4. **Irresponsible communication.** Failing to responsibly present
content which includes, links to, or describes any other restricted
behaviors.
## Reporting
If something here is being broken, contact me directly. Do not open a
public issue.
| Channel | Address |
| ---------- | ------------------------ |
| Email | `kontakt@hellion-media.de` |
| Discord DM | `@j.j_kazama` |
Reports stay private. I will acknowledge within a few weekdays
(European business hours) and tell you what I plan to do.
## Enforcement
I am the sole maintainer, so enforcement is a single-person process.
I will pick the lightest measure that actually resolves the situation:
1. Private note asking the behaviour to stop.
2. Public correction in the affected thread.
3. Edit or removal of the offending content.
4. Private written warning with a cooldown period.
5. Temporary block from the repository or related spaces.
6. Permanent block.
Severe cases skip the lower steps. I will not negotiate over harassment
or threats.
## Scope
This Code of Conduct applies to all spaces the project owns or that I
run on its behalf: the GitHub repository, GitHub Discussions,
project-related Discord conversations, and the maintainer contact
listed in [`SECURITY.md`](SECURITY.md). It also applies when someone
is identifiably representing HellionChat elsewhere, for example when
posting as a HellionChat maintainer in the Dalamud Discord.
## Attribution
This Code of Conduct is adapted from the Contributor Covenant, version
3.0, available at
[https://www.contributor-covenant.org/version/3/0/](https://www.contributor-covenant.org/version/3/0/).
Contributor Covenant is stewarded by the Organization for Ethical
Source and licensed under CC BY-SA 4.0. To view a copy of this
license, visit
[https://creativecommons.org/licenses/by-sa/4.0/](https://creativecommons.org/licenses/by-sa/4.0/).
+147
View File
@@ -0,0 +1,147 @@
# Contributing to HellionChat
Thanks for taking a look. HellionChat is a one-person side project
developed under Hellion Forge. It started as a fork of
[Chat 2](https://github.com/Infiziert90/ChatTwo) and has since become
a standalone plugin under its own namespace, IPC channels and
source tree (standalone-cut completed in v1.0.0). Forking HellionChat
itself is explicitly permitted under the EUPL-1.2.
This document explains what I am looking for, what I am not, and how
to make a contribution land smoothly.
## Before You Open Anything
- Read the [README](README.md) so you understand the scope: a
privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally
removes the upstream webinterface and ships privacy-first defaults.
- Read [`docs/UPSTREAM_SYNC.md`](docs/UPSTREAM_SYNC.md). Cherry-picks
from upstream Chat 2 are selective and deliberate; not everything
that lands there belongs here.
- Read [`SECURITY.md`](SECURITY.md). Anything security-sensitive goes
through a private advisory, never a public issue or PR.
- Read the [Code of Conduct](CODE_OF_CONDUCT.md).
## What I Will Accept
- 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
`HellionChat/Resources/HellionStrings.*.resx`. Translations for
upstream Chat 2 strings (`Language.*.resx`) are not handled here;
those go to the upstream Chat 2 project.
- Documentation improvements (README, comments, this file).
- Performance fixes with a measurable before/after.
- New features that fit the privacy-first scope and do not duplicate
what an existing Dalamud plugin already does well.
## What I Will Probably Decline
- Re-introducing the webinterface or any remote-access feature. It was
removed in v0.2.0 on purpose. See the README section
"Was gegenüber Chat 2 fehlt".
- Features that bypass the privacy filter or weaken the default
retention behaviour without an explicit, documented opt-in.
- Sweeping refactors that touch large parts of the codebase. 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
[`docs/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
first and ask before writing code. I would rather say "no" to a
proposal than to a finished pull request.
## Workflow
1. Open an issue (bug or feature request) using the templates under
`.github/ISSUE_TEMPLATE/`. Skip this for trivial typos.
2. Fork the repository and branch off `main`. Branch naming is
informal; something like `fix/auto-tell-history-empty` or
`feat/theme-export` is fine.
3. Match the existing code style. The repository ships an
`.editorconfig` that VS Code and Rider pick up automatically.
4. Keep commits focused. Several small commits with clear messages are
easier to review than one large 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 `HellionChat/HellionChat.yaml` and
`repo.json`. I bump the version number myself at release time.
6. Open the pull request against `main`. The PR template will ask
you to summarise the change, the testing you did and any
compatibility notes.
## Build and Test
The project targets `net10.0-windows` against Dalamud SDK 15. To build
locally you need:
- .NET 10 SDK
- A working Dalamud dev environment with `DALAMUD_HOME` set
(XIVLauncher installed and launched once is the simplest path)
- VS Code with the C# Dev Kit, Rider, or Visual Studio
```bash
dotnet restore
dotnet build HellionChat.sln -c Release
```
There are currently no tests in `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 via `/xlplugins`.
## Continuous Integration
Every push and every pull request runs:
| Workflow | What it checks |
| ------------- | ------------------------------------- |
| `build.yml` | `dotnet build` and `dotnet test` |
| `codeql.yml` | CodeQL security analysis |
A pull request will not be merged while either of these is failing.
CodeQL findings on changed code need to be addressed; pre-existing
findings on untouched code are tracked separately.
## Translations
Hellion-specific strings live in
`HellionChat/Resources/HellionStrings.resx` (English source) and
`HellionStrings.<lang>.resx` (per-language). These are accepted as
direct pull requests.
The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx`
are **not** translated here. They are owned by the upstream project
and synced in via cherry-pick. Please contribute those to
[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo) instead.
## Licensing
By submitting a pull request you confirm that:
- Your contribution is your own work, or you have the right to
contribute it under the project licence.
- You agree that your contribution will be released under the
[EUPL-1.2](LICENSE), the same licence as the rest of the project.
There is no separate CLA. Forking HellionChat is explicitly permitted
under the EUPL-1.2, as with any EUPL-licensed project.
## Response Times
| Channel | Address |
| ------------- | -------------------------- |
| GitHub Issues | Preferred for bugs and feature requests |
| Discord DM | `@j.j_kazama` |
| Email | `kontakt@hellion-media.de` |
I respond on weekdays during European business hours and take weekends
and FFXIV patch days off. A pull request that sits for a few days has
not been ignored. Pinging once after a week is fine; please do not
ping daily.
+51
View File
@@ -0,0 +1,51 @@
HellionChat — a privacy-focused fork of ChatTwo for FINAL FANTASY XIV
═══════════════════════════════════════════════════════════════════
Source code
═══════════════════════════════════════════════════════════════════
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
hooks, the localisation infrastructure and most of the
architecture HellionChat still relies on.
Copyright (c) 2025-2026 Florian Wathling / Hellion Online Media
HellionChat-specific modifications, including the privacy filter,
per-channel retention sweep, export pipeline, Auto-Tell-Tabs,
German localisation and the EUPL-1.2 fork maintenance.
Source code is licensed under the European Union Public Licence
(EUPL), Version 1.2 only. The full Licence text lives in the LICENSE
file at the root of this repository. The official Licence website is
at: https://eupl.eu/1.2/en/
This Work is provided "AS IS" without warranties of any kind. See
Article 7 (Disclaimer of Warranty) and Article 8 (Disclaimer of
Liability) of the Licence for the legally binding wording.
═══════════════════════════════════════════════════════════════════
Visual assets
═══════════════════════════════════════════════════════════════════
Copyright (c) 2026 Florian Eck
Designer of the Hellion Forge logo and Hellion Online Media logo
(variants located in docs/images and HellionChat/images).
Exclusive usage and marketing rights licensed to Hellion Online
Media. These assets are NOT covered by the EUPL-1.2 source code
licence above and may not be reused, modified, or redistributed
without separate permission from the copyright holder.
═══════════════════════════════════════════════════════════════════
Bundled assets
═══════════════════════════════════════════════════════════════════
Exo 2 font (HellionChat/Resources/HellionFont.ttf)
SIL Open Font License 1.1, full text in HellionFont-OFL.txt.
Bundled with permission per the OFL terms.
═══════════════════════════════════════════════════════════════════
Acknowledgements directed at the upstream ChatTwo authors live in
NOTICE.md. The manual upstream-sync workflow lives in UPSTREAM_SYNC.md.
-167
View File
@@ -1,167 +0,0 @@
using System;
using System.IO;
using System.Linq;
using ChatTwo.Code;
using ChatTwo.Util;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace ChatTwo.Tests;
// Hellion Chat — Auto-Tell-Tabs history-preload coverage.
//
// These tests exercise MessageStore.GetTellHistoryWithSender, the query the
// AutoTellTabsService uses to populate a freshly spawned temp tab with the
// last conversations with that player.
//
// NOTE: like the rest of ChatTwo.Tests today, these will fail at runtime
// until the project's Dalamud.dll runtime dependency is sorted out (see
// Phase-2 backlog item "Test-Projekt fixen"). Compile-time the suite builds
// fine via DALAMUD_HOME, so the tests guard against API drift even before
// they can be executed locally.
[TestClass]
[TestSubject(typeof(MessageStore))]
public class AutoTellTabsHistoryTest
{
public TestContext TestContext { get; set; }
[TestMethod]
[Timeout(5000)]
public void GetTellHistoryWithSender_FiltersByNameAndWorld()
{
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
const ulong receiver = 99001;
var now = DateTimeOffset.UtcNow;
// Two tells with the target sender, one with a different sender on
// the same world, one with the same name on a different world. Only
// the first two should make it into the result.
var asukaLichIn = TellMessage("Asuka", 76, receiver, now.AddMinutes(-30), ChatType.TellIncoming);
var asukaLichOut = TellMessage("Asuka", 76, receiver, now.AddMinutes(-20), ChatType.TellOutgoing);
var broboLich = TellMessage("Brobo", 76, receiver, now.AddMinutes(-10), ChatType.TellIncoming);
var asukaOmega = TellMessage("Asuka", 90, receiver, now.AddMinutes(-5), ChatType.TellIncoming);
store.UpsertMessage(asukaLichIn);
store.UpsertMessage(asukaLichOut);
store.UpsertMessage(broboLich);
store.UpsertMessage(asukaOmega);
var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 50);
Assert.AreEqual(2, result.Count);
// Result is oldest-first so a tab can append messages chronologically.
Assert.AreEqual(asukaLichIn.Id, result[0].Id);
Assert.AreEqual(asukaLichOut.Id, result[1].Id);
}
[TestMethod]
[Timeout(5000)]
public void GetTellHistoryWithSender_RespectsLimit()
{
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
const ulong receiver = 99002;
var now = DateTimeOffset.UtcNow;
for (var i = 0; i < 30; i++)
{
var msg = TellMessage("Asuka", 76, receiver, now.AddMinutes(-i - 1), ChatType.TellIncoming);
store.UpsertMessage(msg);
}
var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 5);
Assert.AreEqual(5, result.Count);
}
[TestMethod]
[Timeout(5000)]
public void GetTellHistoryWithSender_ZeroLimitReturnsEmpty()
{
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
const ulong receiver = 99003;
var msg = TellMessage("Asuka", 76, receiver, DateTimeOffset.UtcNow, ChatType.TellIncoming);
store.UpsertMessage(msg);
var result = store.GetTellHistoryWithSender(receiver, "Asuka", 76, limit: 0);
Assert.AreEqual(0, result.Count);
}
[TestMethod]
[Timeout(5000)]
public void GetTellHistoryWithSender_IgnoresOtherReceivers()
{
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
const ulong ourReceiver = 99004;
const ulong otherReceiver = 99005;
var now = DateTimeOffset.UtcNow;
// Tell on the local player's account.
var ours = TellMessage("Asuka", 76, ourReceiver, now.AddMinutes(-1), ChatType.TellIncoming);
// Same sender, but logged against a different local character —
// common when the user has alts. Must not surface.
var foreign = TellMessage("Asuka", 76, otherReceiver, now, ChatType.TellIncoming);
store.UpsertMessage(ours);
store.UpsertMessage(foreign);
var result = store.GetTellHistoryWithSender(ourReceiver, "Asuka", 76, limit: 50);
Assert.AreEqual(1, result.Count);
Assert.AreEqual(ours.Id, result[0].Id);
}
private static Message TellMessage(
string senderName,
uint senderWorld,
ulong receiver,
DateTimeOffset dateTime,
ChatType chatType)
{
var senderSeString = new SeStringBuilder()
.Add(new PlayerPayload(senderName, senderWorld))
.AddText(senderName)
.Add(RawPayload.LinkTerminator)
.Build();
var contentSeString = new SeStringBuilder()
.AddText("test message")
.Build();
var senderChunks = ChunkUtil.ToChunks(senderSeString, ChunkSource.Sender, chatType).ToList();
var contentChunks = ChunkUtil.ToChunks(contentSeString, ChunkSource.Content, chatType).ToList();
var chatCode = new ChatCode((XivChatType)chatType, XivChatRelationKind.LocalPlayer, XivChatRelationKind.LocalPlayer);
return new Message(
Guid.NewGuid(),
receiver,
0,
dateTime,
chatCode,
senderChunks,
contentChunks,
senderSeString,
contentSeString,
Guid.Empty);
}
}
-53
View File
@@ -1,53 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0-windows</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="morelinq" Version="4.4.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.6.3" />
<PackageReference Include="MSTest.TestFramework" Version="3.6.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ChatTwo\ChatTwo.csproj" />
</ItemGroup>
<PropertyGroup>
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$(IsCI)' == 'true'">
<DalamudLibPath>$(HOME)/dalamud</DalamudLibPath>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)\Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)\Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)\Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
-293
View File
@@ -1,293 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ChatTwo.Code;
using ChatTwo.Util;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using JetBrains.Annotations;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Chat2PartyFinderPayload = ChatTwo.Util.PartyFinderPayload;
namespace ChatTwo.Tests;
[TestClass]
[TestSubject(typeof(MessageStore))]
public class MessageStoreTest {
// From Message.cs
private static readonly byte[] ExtraChatChannelPayloadBytes = [0, 0x27, 18, 0x20];
public TestContext TestContext { get; set; }
public static string GetImportPath() {
string[] importPaths = [
@".\TestData",
@"..\TestData",
@"..\..\TestData",
@"..\..\..\TestData",
];
var importPath = importPaths.FirstOrDefault(Directory.Exists);
if (string.IsNullOrEmpty(importPath)) {
throw new DirectoryNotFoundException("Could not find the import path");
}
return importPath;
}
[TestMethod]
[Timeout(5000)]
public void StoreAndRetrieve() {
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
// Write the message.
var input = BigMessage();
store.UpsertMessage(input);
// Read the message back.
using var messageEnumerator = store.GetMostRecentMessages();
var messages = messageEnumerator.ToList();
Assert.AreEqual(1, messages.Count);
AssertMessagesEqual(input, messages.First());
}
[TestMethod]
[Timeout(5000)]
public void RetrieveMultiple() {
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
// Insert 10 messages in the wrong order of date.
var messages = new List<Message>();
const uint receiver = 12345;
var now = DateTimeOffset.UtcNow;
for (var i = 0; i < 10; i++) {
var message = BigMessage(true, receiver, now.AddSeconds(-i));
TestContext.WriteLine($"Inserting message {i}: {message.Id}");
store.UpsertMessage(message);
messages.Add(message);
}
// Insert a message for a different receiver. This shouldn't be returned
// because of the receiver filtering.
var otherReceiverMsg = BigMessage(receiver: receiver + 1, dateTime: now.AddSeconds(1));
TestContext.WriteLine($"Inserting other receiver message: {otherReceiverMsg.Id}");
store.UpsertMessage(otherReceiverMsg);
// Query the most recent 5 messages. Should return the 4 newest messages
// from the list, as well as the different receiver message because we
// aren't filtering.
using var unfilteredMessageEnumerator = store.GetMostRecentMessages(count: 5);
var outputMessages = unfilteredMessageEnumerator.ToList();
var gotIds = outputMessages.Select(m => m.Id).ToList();
TestContext.WriteLine($"Query 1 got IDs: {string.Join(", ", gotIds)}");
AssertGuidsEqual(new List<Guid> {
messages[3].Id,
messages[2].Id,
messages[1].Id,
messages[0].Id,
otherReceiverMsg.Id
}, gotIds);
// Query the most recent 5 messages but filter by receiver ID.
using var filteredByReceiverMessageEnumerator = store.GetMostRecentMessages(receiver: receiver, count: 5);
outputMessages = filteredByReceiverMessageEnumerator.ToList();
gotIds = outputMessages.Select(m => m.Id).ToList();
TestContext.WriteLine($"Query 2 got IDs: {string.Join(", ", gotIds)}");
AssertGuidsEqual(new List<Guid> {
messages[4].Id,
messages[3].Id,
messages[2].Id,
messages[1].Id,
messages[0].Id,
}, gotIds);
// Query the most recent 5 messages but only since a specific date.
using var filteredByReceiverAndDateMessageEnumerator = store.GetMostRecentMessages(receiver, since: messages[1].Date, count: 5);
outputMessages = filteredByReceiverAndDateMessageEnumerator.ToList();
gotIds = outputMessages.Select(m => m.Id).ToList();
TestContext.WriteLine($"Query 3 got IDs: {string.Join(", ", gotIds)}");
AssertGuidsEqual(new List<Guid> {
messages[1].Id,
messages[0].Id,
}, gotIds);
}
[TestMethod]
[Timeout(5000)]
// This test guards against the data format changing in an incompatible way.
public void RetrieveExisting() {
var input = BigMessage(uniqId: false);
var dbPath = Path.Join(GetImportPath(), "existing.db");
TestContext.WriteLine($"Using existing database: {dbPath}");
Assert.IsTrue(File.Exists(dbPath));
// Uncomment this section to regenerate the existing database.
/*
File.Delete(dbPath);
using (var newStore = new MessageStore(dbPath)) {
newStore.UpsertMessage(input);
}
*/
using var store = new MessageStore(dbPath);
using var existingMessageEnumerator = store.GetMostRecentMessages();
var output = existingMessageEnumerator.ToList();
Assert.AreEqual(1, output.Count);
AssertMessagesEqual(input, output[0]);
}
[TestMethod]
[Timeout(30_000)]
public void ProfileMany() {
const int count = 20_000;
var tempDir = Directory.CreateTempSubdirectory("ChatTwo_test_");
var dbPath = Path.Join(tempDir.FullName, "test.db");
TestContext.WriteLine("Using database path: " + dbPath);
using var store = new MessageStore(dbPath);
for (var i = 0; i < count; i++) {
var message = BigMessage(uniqId: true);
store.UpsertMessage(message);
}
using var messageEnumerator = store.GetMostRecentMessages(count: count);
var messages = messageEnumerator.ToList();
Assert.AreEqual(count, messages.Count);
foreach (var message in messages) {
// Load the message because they are lazily parsed.
Assert.IsTrue(message.Id != Guid.Empty);
}
}
internal static Message BigMessage(bool uniqId = true, uint receiver = 12345, DateTimeOffset? dateTime = null) {
// NOTE: These values aren't valid in the game.
// NOTE: we can't test UiForeground, UiGlow, or AutoTranslatePayload
// because they load data from the game.
var senderSeString = new SeStringBuilder()
.AddText("<")
.Add(new PlayerPayload("Player Name", 12345))
.AddItalics("Player Name")
.Add(RawPayload.LinkTerminator)
.AddText(">: ")
.Build();
var extraChatId = Guid.Parse("03d9e6d4-dc1a-4005-bbe7-66b8c3529277");
var contentSeString = new SeStringBuilder()
.Add(new RawPayload(ExtraChatChannelPayloadBytes.Concat(extraChatId.ToByteArray()).ToArray()))
.AddIcon(BitmapFontIcon.IslandSanctuary)
.AddMapLink(1, 2, 3, 4)
.AddText("map")
.Add(RawPayload.LinkTerminator)
.AddQuestLink(12345)
.AddText("quest")
.Add(RawPayload.LinkTerminator)
.Add(new DalamudLinkPayload())
.AddText("dalamud")
.Add(RawPayload.LinkTerminator)
.AddStatusLink(12345)
.AddText("status")
.Add(RawPayload.LinkTerminator)
.AddPartyFinderLink(12345)
.AddText("party finder")
.Add(RawPayload.LinkTerminator)
.Build();
// Add Chat 2 specific payloads (that can't be serialized into the
// SeString).
var contentChunks = ChunkUtil.ToChunks(contentSeString, ChunkSource.Content, ChatType.Say).ToList();
contentChunks = contentChunks.Concat([
new TextChunk(ChunkSource.Content, new Chat2PartyFinderPayload(12345), "chat 2 party finder"),
new TextChunk(ChunkSource.Content, new AchievementPayload(12345), "chat 2 achievement"),
new TextChunk(ChunkSource.Content, new UriPayload(new Uri("https://dalamud.dev")), "chat 2 uri"),
]).ToList();
var chatCode = new ChatCode((XivChatType)46, XivChatRelationKind.LocalPlayer, XivChatRelationKind.EngagedEnemy);
return new Message(
uniqId ? Guid.NewGuid() : Guid.Parse("f011343e-6a21-49e5-a6f9-238f0f1f8c2c"),
receiver,
54321,
dateTime ?? DateTimeOffset.FromUnixTimeMilliseconds(1713520182440),
chatCode,
ChunkUtil.ToChunks(senderSeString, ChunkSource.Sender, ChatType.Debug).ToList(),
contentChunks,
senderSeString,
contentSeString,
extraChatId
);
}
internal static void AssertMessagesEqual(Message input, Message output) {
// Check basic fields.
Assert.AreEqual(input.Id, output.Id);
Assert.AreEqual(input.Receiver, output.Receiver);
Assert.AreEqual(input.ContentId, output.ContentId);
// Assert time is within 1 second
var timeDifference = Math.Abs(input.Date.ToUniversalTime().Subtract(output.Date.ToUniversalTime()).TotalSeconds);
Assert.IsTrue(timeDifference < 1);
Assert.AreEqual(input.Code, output.Code);
Assert.AreEqual($"{input.SenderSource.Encode():X}", $"{output.SenderSource.Encode():X}");
Assert.AreEqual($"{input.ContentSource.Encode():X}", $"{output.ContentSource.Encode():X}");
Assert.AreEqual(input.SortCodeV2, output.SortCodeV2);
Assert.AreEqual(input.ExtraChatChannel, output.ExtraChatChannel);
// Check chunks.
AssertChunksEqual(input.Sender, output.Sender);
AssertChunksEqual(input.Content, output.Content);
}
private static void AssertChunksEqual(IReadOnlyList<Chunk> inputChunks, IReadOnlyList<Chunk> outputChunks) {
Assert.AreEqual(inputChunks.Count, outputChunks.Count);
for (var i = 0; i < inputChunks.Count; i++) {
var inputChunk = inputChunks[i];
var outputChunk = outputChunks[i];
Assert.AreEqual(inputChunk.Source, outputChunk.Source);
switch (inputChunk.Link) {
case AchievementPayload inputAchievementPayload:
Assert.AreEqual(inputAchievementPayload.Id, ((AchievementPayload) outputChunk.Link)!.Id);
break;
case Chat2PartyFinderPayload inputPartyFinderPayload:
Assert.AreEqual(inputPartyFinderPayload.Id, ((Chat2PartyFinderPayload) outputChunk.Link)!.Id);
break;
case UriPayload inputUriPayload:
Assert.AreEqual(inputUriPayload.Uri, ((UriPayload) outputChunk.Link)!.Uri);
break;
case null:
Assert.IsTrue(outputChunk.Link == null);
break;
default:
Assert.AreEqual($"{inputChunk.Link.Encode():X}", $"{outputChunk.Link!.Encode():X}");
break;
}
switch (inputChunk) {
case TextChunk inputTextChunk:
var outputTextChunk = (TextChunk)outputChunk;
Assert.AreEqual(inputTextChunk.FallbackColour, outputTextChunk.FallbackColour);
Assert.AreEqual(inputTextChunk.Foreground, outputTextChunk.Foreground);
Assert.AreEqual(inputTextChunk.Glow, outputTextChunk.Glow);
Assert.AreEqual(inputTextChunk.Italic, outputTextChunk.Italic);
Assert.AreEqual(inputTextChunk.Content, outputTextChunk.Content);
break;
case IconChunk inputIconChunk:
Assert.AreEqual(inputIconChunk.Icon, ((IconChunk) outputChunk).Icon);
break;
default:
throw new Exception("Unknown chunk type");
}
}
}
private static void AssertGuidsEqual(IReadOnlyList<Guid> expected, IReadOnlyList<Guid> got) {
Assert.AreEqual(expected.Count, got.Count);
for (var i = 0; i < expected.Count; i++) {
Assert.AreEqual(expected[i].ToString(), got[i].ToString());
}
}
}
Binary file not shown.
-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
-75
View File
@@ -1,75 +0,0 @@
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
<PropertyGroup>
<!-- Hellion Chat versioning runs separately from upstream Chat 2.
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.4.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. -->
<AssemblyName>HellionChat</AssemblyName>
<RootNamespace>ChatTwo</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="9.0.0" />
<PackageReference Include="morelinq" Version="4.4.0" />
<PackageReference Include="Pidgin" Version="3.3.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\Language.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Language.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Language.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Language.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<!-- HellionChat — Hellion-specific resource bundle (HellionStrings.resx
+ HellionStrings.<lang>.resx) is picked up automatically by the SDK
default include. Designer.cs is hand-maintained, no auto-gen needed. -->
<!-- Bundled Hellion font (Exo 2, OFL-1.1). Embedded as a manifest
resource with a fixed LogicalName so FontManager can pull the
bytes back at runtime via AddFontFromMemory. The OFL license
text travels with it inside the assembly to satisfy the
"license must be distributed with the font" clause. -->
<ItemGroup>
<EmbeddedResource Include="Resources\HellionFont.ttf">
<LogicalName>HellionFont.ttf</LogicalName>
</EmbeddedResource>
<EmbeddedResource Include="Resources\HellionFont-OFL.txt">
<LogicalName>HellionFont-OFL.txt</LogicalName>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="images\" />
</ItemGroup>
<!-- Copy images/icon.png next to the built DLL so Dalamud's local
plugin loader finds it at <plugindir>/images/icon.png. The
DalamudPackager.targets file in this directory then includes
the same path inside the release ZIP — see that file for the
full packaging override. -->
<ItemGroup>
<None Include="images\icon.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
-76
View File
@@ -1,76 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
HellionChat — DalamudPackager override.
The default DalamudPackager.targets shipped by the SDK does not set
HandleImages / ImagesPath, so the images/ directory is silently
excluded from the release ZIP. The presence of this file at
$(ProjectDir)DalamudPackager.targets disables the SDK's default
target (it guards on `!Exists('$(PackagerTargetFile)')`) and lets
us call the packager task ourselves with the image fields wired in.
Apart from HandleImages + ImagesPath the property list mirrors the
SDK default verbatim so we don't lose any other manifest field as
the upstream SDK evolves.
-->
<Project>
<Target Name="HellionDalamudPackagerDebug"
AfterTargets="Build"
Condition="'$(Configuration)' == 'Debug'">
<DalamudPackager ProjectDir="$(ProjectDir)"
OutputPath="$(OutputPath)"
AssemblyName="$(AssemblyName)"
MakeZip="false"
Author="$(Author)"
Name="$(Name)"
MinimumDalamudVersion="$(MinimumDalamudVersion)"
Punchline="$(Punchline)"
Description="$(Description)"
ApplicableVersion="$(ApplicableVersion)"
RepoUrl="$(RepoUrl)"
Tags="$(Tags)"
CategoryTags="$(CategoryTags)"
DalamudApiLevel="$(DalamudApiLevel)"
LoadRequiredState="$(LoadRequiredState)"
LoadSync="$(LoadSync)"
CanUnloadAsync="$(CanUnloadAsync)"
LoadPriority="$(LoadPriority)"
ImageUrls="$(ImageUrls)"
IconUrl="$(IconUrl)"
Changelog="$(Changelog)"
AcceptsFeedback="$(AcceptsFeedback)"
FeedbackMessage="$(FeedbackMessage)"
HandleImages="true"
ImagesPath="$(ProjectDir)images" />
</Target>
<Target Name="HellionDalamudPackagerRelease"
AfterTargets="Build"
Condition="'$(Configuration)' == 'Release'">
<DalamudPackager ProjectDir="$(ProjectDir)"
OutputPath="$(OutputPath)"
AssemblyName="$(AssemblyName)"
MakeZip="true"
Author="$(Author)"
Name="$(Name)"
MinimumDalamudVersion="$(MinimumDalamudVersion)"
Punchline="$(Punchline)"
Description="$(Description)"
ApplicableVersion="$(ApplicableVersion)"
RepoUrl="$(RepoUrl)"
Tags="$(Tags)"
CategoryTags="$(CategoryTags)"
DalamudApiLevel="$(DalamudApiLevel)"
LoadRequiredState="$(LoadRequiredState)"
LoadSync="$(LoadSync)"
CanUnloadAsync="$(CanUnloadAsync)"
LoadPriority="$(LoadPriority)"
ImageUrls="$(ImageUrls)"
IconUrl="$(IconUrl)"
Changelog="$(Changelog)"
AcceptsFeedback="$(AcceptsFeedback)"
FeedbackMessage="$(FeedbackMessage)"
HandleImages="true"
ImagesPath="$(ProjectDir)images" />
</Target>
</Project>
-254
View File
@@ -1,254 +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.
repo_url: https://github.com/JonKazama-Hellion/HellionChat
accepts_feedback: true
tags:
- Social
- UI
- Chat
- Replacement
- Privacy
changelog: |-
**Hellion Chat 0.4.0 — Auto-Tell-Tabs**
Auto-Tell-Tabs lets you turn each /tell into a session-only tab
dedicated to that conversation partner. The original use case is
the FFXIV club greeter who has to track 515 parallel "hi, welcome"
exchanges; everyone else can disable the feature in one click and
go back to a single Tell Exclusive tab.
What lands in this release:
- Auto-spawn temp tab "Name@World" on /tell (incoming and outgoing)
- Tab limit (default 15, range 150) with LRU drop that prefers
greeted tabs first, then sorts by last activity
- History preload from the local message store (default 20 tells,
range 0100) with a "— Earlier conversations —" separator above
the live tell that triggered the spawn
- Optional "mark as greeted" toggle button (off by default,
greeter-specific) that dims the tab name and lets you flip the
status
- Section header "Active Tells (n)" or compact-mode separator in
the sidebar between persistent tabs and the temp tabs
- Settings UI under Chat (toggle / limit / compact / greeted-toggle)
and Privacy (history preload count), with hover-tooltip help
markers replacing the previous wall-of-text descriptions for the
new sections
- Save and load filters strip temp tabs from the on-disk config so
a crash or a sidebar-mode toggle never persists or wipes them
Compatibility note: if XIV Messanger or another plugin is
suppressing direct messages, disable its "Suppress DMs" option so
Hellion Chat can receive tells and open the auto tabs.
Configuration version bumps from 8 to 9. Existing users get a one-
shot notification on the first start, defaults are seeded by
property initializers, persistent tabs are untouched.
The vertical sidebar tab view becomes the default for fresh
installs; existing users keep their saved preference.
Inspired by the per-sender tab pattern in XIV InstantMessenger
(Limiana, AGPL-3.0). No code was ported across the licence
boundary; only the architectural concept influenced this design.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.3.1 — Upstream emote regression fix**
Cherry-picks Infi's upstream commit ff899ff "Fix a regression
from API 15 updates" which changes the BetterTTV emote DTOs
(Emote and Top100) from public fields to public properties.
System.Text.Json under the API 15 toolchain only honours the
[JsonPropertyName] attribute on properties, so the previous
field-based version deserialised every fetched emote into empty
default values. Result: BetterTTV emotes were silently broken
on fresh installs. The fix is six lines and applies cleanly on
top of our defensive null-check from earlier; the EmoteCache
path-traversal hardening from 0.3.0 stays as it is.
Authorship of the fix is preserved with git cherry-pick -x, so
Infi shows up as the author on the commit. Thanks to him for
catching it in the upstream codebase.
**Hellion Chat 0.3.0 — Audit hardening, brand sweep and rebrand of slash commands**
This release closes the remaining audit follow-ups from the
0.2.0 cleanup and finishes turning Hellion Chat into a properly
branded fork rather than a Chat 2 with a different name.
Slash commands have been renamed across the board so they no
longer collide with the upstream plugin and tell you which
plugin owns them at a glance:
- /chat2 becomes /hellion
- /chat2Viewer becomes /hellionView
- /clearlog2 becomes /clearhellion
- /chat2Debugger becomes /hellionDebugger (internal)
- /chat2SeString becomes /hellionSeString (internal)
This is a breaking change for anyone with macros bound to the
old command names. The upstream Chat 2 commands keep working
if you also have that plugin installed.
Privacy and storage hardening based on the post-0.2.0 audit:
- Privacy filter master switch now states explicitly that the
filter only governs storage, not the live chat log
- Emote cache refuses to write outside its own directory if a
third-party API ever returns a path that escapes
- Retention sweep is serialised so the 24h auto-sweep and the
manual button cannot launch in parallel and race for the
SQLite connection
- DbViewer paging uses an int constant and the matching SQL
parameter name (the upstream code passed a float and a name
without the parameter prefix; both worked in practice but
were inconsistent)
Visual identity now matches the Hellion Online Media website:
- Theme palette switched to Arctic Cyan plus Ember Orange,
matching the website's BRANDING.md tokens
- Active tabs and window title bars use a brand-color-dark teal
variation as identity colour, replacing the previous slate
violet that did not appear in the brand
- Resize grips and scrollbar grabs picked up Ember Orange
instead of industrial amber on hover and active states
About tab rewritten and properly localised:
- New "Why this fork exists" block sets out the mission in
neutral terms, framing Chat 2's full-history default as the
right one for most users while explaining the narrower
default footprint this fork chose
- All Hellion-specific About copy now lives in HellionStrings
in EN and DE, so German users see the Hellion sections in
German rather than the upstream English fallback
- Webinterface absence is described as a focus mismatch
(different use case, substantial rebuild) rather than as
a security issue with the upstream code
- Translator list at the bottom of the About tab is reachable
again on smaller settings windows
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.2.0 — Webinterface removed**
The upstream webinterface has been removed in its entirety. It
serves a different use case from the smaller default footprint
this fork is built around, namely remote access to chat from a
second device. Aligning it with the data minimisation defaults
Hellion Chat ships with would have meant a substantial rebuild.
Removing it was the cleaner path for this particular fork.
What changed in this release:
- Settings tab "Webinterface" is gone, the corresponding
Configuration fields (WebinterfaceEnabled / AutoStart / Password /
Port / AuthStore / MaxLinesToSend) are dropped and stale entries
fall out of the JSON on the next save automatically
- The whole ChatTwo/Http tree, the bundled Svelte frontend in
websiteBuild.zip and the WebinterfaceUtil helper are deleted
- Watson.Lite (the HTTP server) and Newtonsoft.Json (only used by
the webinterface JSON wire format) are removed from the
package references
- DbViewer's "Chat2 JSON Export" button is dropped because it
serialised the database into the webinterface message protocol;
the Privacy tab's MessageExporter (Markdown, JSON, CSV with
channel and date filters) covers the same ground without the
proprietary shape
- About tab notes the absence so users coming from Chat 2 do not
look for it
- Configuration version bumps from 7 to 8 with a one-shot
notification (EN + DE)
No changes to the privacy filter, retention sweep, first-run wizard
or export pipeline. Existing chat history is preserved.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.1.2 — About tab rebrand, DBViewer polish**
- About tab now shows Hellion-specific maintainer, license, EU/US/JP
disclaimer and SQUARE ENIX disclaimer instead of the inherited
Chat 2 contact info; original ChatTwo translator credits stay
visible under a clearly labelled upstream tree node
- Localization clarified: Hellion-specific German strings are
maintained by the fork maintainer, the Crowdin contributor list
only covers the inherited upstream strings
- Cherry-picked DBViewer UI improvements from upstream Chat 2
(auto-scroll-reset on page change, tooltips on date reset,
folder export, page arrows, localized export-running messages)
- README rewritten in the Hellion project style with a tech-stack
table, architecture tree, database column list, install guide,
upstream-sync workflow notes and project-status checklist
**Hellion Chat 0.1.1 — Packaging and migration fixes**
- Plugin icon now ships inside the bundle, so the Hellion logo
renders locally in the Dalamud plugin list once installed (the
previous release relied only on the remote IconUrl)
- Plugin icon downsampled from 1024×1024 to 256×256 to match the
rendered size; loads faster and caches better
- Migration from upstream Chat 2 is more robust: each file move is
wrapped individually, a locked SQLite database no longer aborts
the rest of the migration, and a warning notification fires when
any file is held open (with a hint to disable Chat 2 and restart
the game)
- README ships a step-by-step migration guide (fresh install versus
coming from Chat 2) and a troubleshooting section with manual
recovery commands for Linux and Windows
**Hellion Chat 0.1.0 — Initial fork release**
Privacy
- Channel whitelist filter in MessageStore.UpsertMessage with a
Privacy-First default (own conversations only)
- Per-channel retention with a 24-hour idempotent background sweep
- Retroactive cleanup with a Ctrl+Shift confirm and VACUUM
- Export to Markdown / JSON / CSV via Dalamud's file dialog
Onboarding
- First-run wizard with three profiles: Privacy-First / Casual /
Full History
- Configuration migration that seeds defaults on update
- One-shot migration from upstream Chat 2's pluginConfigs layout
- Migrate3 idempotency recovery for half-migrated databases
Look & feel
- Localized UI (English and German) with live language switching
- Industrial HUD theme with cyan-teal action accents, slate-violet
tabs, amber active highlights and a window-opacity slider
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
-1
View File
@@ -1 +0,0 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ChatTwo.Tests")]
-195
View File
@@ -1,195 +0,0 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Hand-maintained strongly-typed accessor for HellionStrings.resx.
// Mirrors the layout of Language.Designer.cs so the same Plugin.cs
// LanguageChanged handler can update Culture for both classes.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable
namespace ChatTwo.Resources;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
internal class HellionStrings
{
private static global::System.Resources.ResourceManager? resourceMan;
private static global::System.Globalization.CultureInfo? resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal HellionStrings() { }
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if (resourceMan is null)
resourceMan = new global::System.Resources.ResourceManager("ChatTwo.Resources.HellionStrings", typeof(HellionStrings).Assembly);
return resourceMan;
}
}
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo? Culture
{
get => resourceCulture;
set => resourceCulture = value;
}
private static string Get(string key)
=> ResourceManager.GetString(key, resourceCulture) ?? key;
internal static string Privacy_Tab_Title => Get(nameof(Privacy_Tab_Title));
internal static string Privacy_FilterEnabled_Name => Get(nameof(Privacy_FilterEnabled_Name));
internal static string Privacy_FilterEnabled_Description => Get(nameof(Privacy_FilterEnabled_Description));
internal static string Privacy_FilterEnabled_StorageOnly_Help => Get(nameof(Privacy_FilterEnabled_StorageOnly_Help));
internal static string Privacy_Whitelist_Help => Get(nameof(Privacy_Whitelist_Help));
internal static string Privacy_Preset_PrivacyFirst => Get(nameof(Privacy_Preset_PrivacyFirst));
internal static string Privacy_Preset_ClearAll => Get(nameof(Privacy_Preset_ClearAll));
internal static string Privacy_Preset_SelectAll => Get(nameof(Privacy_Preset_SelectAll));
internal static string Privacy_Group_DirectMessages => Get(nameof(Privacy_Group_DirectMessages));
internal static string Privacy_Group_PartyAlliance => Get(nameof(Privacy_Group_PartyAlliance));
internal static string Privacy_Group_FreeCompany => Get(nameof(Privacy_Group_FreeCompany));
internal static string Privacy_Group_Linkshells => Get(nameof(Privacy_Group_Linkshells));
internal static string Privacy_Group_CrossLinkshells => Get(nameof(Privacy_Group_CrossLinkshells));
internal static string Privacy_Group_ExtraChat => Get(nameof(Privacy_Group_ExtraChat));
internal static string Privacy_Group_PublicChat => Get(nameof(Privacy_Group_PublicChat));
internal static string Privacy_Group_SystemLogs => Get(nameof(Privacy_Group_SystemLogs));
internal static string Privacy_PersistUnknown_Name => Get(nameof(Privacy_PersistUnknown_Name));
internal static string Privacy_PersistUnknown_Description => Get(nameof(Privacy_PersistUnknown_Description));
internal static string Cleanup_Heading => Get(nameof(Cleanup_Heading));
internal static string Cleanup_Help_Intro => Get(nameof(Cleanup_Help_Intro));
internal static string Cleanup_Help_SavedNote => Get(nameof(Cleanup_Help_SavedNote));
internal static string Cleanup_RefreshPreview => Get(nameof(Cleanup_RefreshPreview));
internal static string Cleanup_NoPreview => Get(nameof(Cleanup_NoPreview));
internal static string Cleanup_TotalStored => Get(nameof(Cleanup_TotalStored));
internal static string Cleanup_WillKeep => Get(nameof(Cleanup_WillKeep));
internal static string Cleanup_WillDelete => Get(nameof(Cleanup_WillDelete));
internal static string Cleanup_Breakdown => Get(nameof(Cleanup_Breakdown));
internal static string Cleanup_Marker_Keep => Get(nameof(Cleanup_Marker_Keep));
internal static string Cleanup_Marker_Delete => Get(nameof(Cleanup_Marker_Delete));
internal static string Cleanup_Apply_Label => Get(nameof(Cleanup_Apply_Label));
internal static string Cleanup_Apply_Tooltip => Get(nameof(Cleanup_Apply_Tooltip));
internal static string Cleanup_Running => Get(nameof(Cleanup_Running));
internal static string Cleanup_PreviewError => Get(nameof(Cleanup_PreviewError));
internal static string Cleanup_Success => Get(nameof(Cleanup_Success));
internal static string Cleanup_Error => Get(nameof(Cleanup_Error));
internal static string Retention_Heading => Get(nameof(Retention_Heading));
internal static string Retention_Enabled_Name => Get(nameof(Retention_Enabled_Name));
internal static string Retention_Enabled_Description => Get(nameof(Retention_Enabled_Description));
internal static string Retention_Default_Label => Get(nameof(Retention_Default_Label));
internal static string Retention_Default_Help => Get(nameof(Retention_Default_Help));
internal static string Retention_Reset_Spec => Get(nameof(Retention_Reset_Spec));
internal static string Retention_Clear_Overrides => Get(nameof(Retention_Clear_Overrides));
internal static string Retention_Tree_Heading => Get(nameof(Retention_Tree_Heading));
internal static string Retention_Tag_Override => Get(nameof(Retention_Tag_Override));
internal static string Retention_Tag_Spec => Get(nameof(Retention_Tag_Spec));
internal static string Retention_Tag_Global => Get(nameof(Retention_Tag_Global));
internal static string Retention_Reset_Button => Get(nameof(Retention_Reset_Button));
internal static string Retention_Apply_Label => Get(nameof(Retention_Apply_Label));
internal static string Retention_Apply_Tooltip => Get(nameof(Retention_Apply_Tooltip));
internal static string Retention_Running => Get(nameof(Retention_Running));
internal static string Retention_LastRun_Never => Get(nameof(Retention_LastRun_Never));
internal static string Retention_LastRun_At => Get(nameof(Retention_LastRun_At));
internal static string Retention_Success => Get(nameof(Retention_Success));
internal static string Retention_Error => Get(nameof(Retention_Error));
internal static string Migration_Notification_Title => Get(nameof(Migration_Notification_Title));
internal static string Migration_Notification_Content => Get(nameof(Migration_Notification_Content));
internal static string Migration_Webinterface_Removed_Title => Get(nameof(Migration_Webinterface_Removed_Title));
internal static string Migration_Webinterface_Removed_Content => Get(nameof(Migration_Webinterface_Removed_Content));
internal static string Wizard_Title => Get(nameof(Wizard_Title));
internal static string Wizard_Intro => Get(nameof(Wizard_Intro));
internal static string Wizard_Profile_PrivacyFirst_Heading => Get(nameof(Wizard_Profile_PrivacyFirst_Heading));
internal static string Wizard_Profile_PrivacyFirst_Description => Get(nameof(Wizard_Profile_PrivacyFirst_Description));
internal static string Wizard_Profile_PrivacyFirst_Apply => Get(nameof(Wizard_Profile_PrivacyFirst_Apply));
internal static string Wizard_Profile_Casual_Heading => Get(nameof(Wizard_Profile_Casual_Heading));
internal static string Wizard_Profile_Casual_Description => Get(nameof(Wizard_Profile_Casual_Description));
internal static string Wizard_Profile_Casual_Apply => Get(nameof(Wizard_Profile_Casual_Apply));
internal static string Wizard_Profile_FullHistory_Heading => Get(nameof(Wizard_Profile_FullHistory_Heading));
internal static string Wizard_Profile_FullHistory_Description => Get(nameof(Wizard_Profile_FullHistory_Description));
internal static string Wizard_Profile_FullHistory_GdprWarning => Get(nameof(Wizard_Profile_FullHistory_GdprWarning));
internal static string Wizard_Profile_FullHistory_Apply => Get(nameof(Wizard_Profile_FullHistory_Apply));
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
internal static string Export_Heading => Get(nameof(Export_Heading));
internal static string Export_Help => Get(nameof(Export_Help));
internal static string Export_Range_Label => Get(nameof(Export_Range_Label));
internal static string Export_Sender_Label => Get(nameof(Export_Sender_Label));
internal static string Export_Channels_Heading => Get(nameof(Export_Channels_Heading));
internal static string Export_Channels_AllOff => Get(nameof(Export_Channels_AllOff));
internal static string Export_Format_Label => Get(nameof(Export_Format_Label));
internal static string Export_Format_Markdown => Get(nameof(Export_Format_Markdown));
internal static string Export_Format_Json => Get(nameof(Export_Format_Json));
internal static string Export_Format_Csv => Get(nameof(Export_Format_Csv));
internal static string Export_Button => Get(nameof(Export_Button));
internal static string Export_Dialog_Title => Get(nameof(Export_Dialog_Title));
internal static string Export_Running => Get(nameof(Export_Running));
internal static string Export_Success => Get(nameof(Export_Success));
internal static string Export_Empty => Get(nameof(Export_Empty));
internal static string Export_Error => Get(nameof(Export_Error));
internal static string Theme_Heading => Get(nameof(Theme_Heading));
internal static string Theme_Enabled_Name => Get(nameof(Theme_Enabled_Name));
internal static string Theme_Enabled_Description => Get(nameof(Theme_Enabled_Description));
internal static string Theme_WindowOpacity_Label => Get(nameof(Theme_WindowOpacity_Label));
internal static string Theme_WindowOpacity_Help => Get(nameof(Theme_WindowOpacity_Help));
internal static string Theme_UseHellionFont_Name => Get(nameof(Theme_UseHellionFont_Name));
internal static string Theme_UseHellionFont_Description => Get(nameof(Theme_UseHellionFont_Description));
internal static string About_Maintainer_Heading => Get(nameof(About_Maintainer_Heading));
internal static string About_Maintainer_Body => Get(nameof(About_Maintainer_Body));
internal static string About_Maintainer_Website_Label => Get(nameof(About_Maintainer_Website_Label));
internal static string About_Mission_Heading => Get(nameof(About_Mission_Heading));
internal static string About_Mission_P1 => Get(nameof(About_Mission_P1));
internal static string About_Mission_P2 => Get(nameof(About_Mission_P2));
internal static string About_Mission_P3 => Get(nameof(About_Mission_P3));
internal static string About_BuiltOn_Heading => Get(nameof(About_BuiltOn_Heading));
internal static string About_BuiltOn_P1 => Get(nameof(About_BuiltOn_P1));
internal static string About_BuiltOn_P2 => Get(nameof(About_BuiltOn_P2));
internal static string About_BuiltOn_Upstream_Label => Get(nameof(About_BuiltOn_Upstream_Label));
internal static string About_License_Heading => Get(nameof(About_License_Heading));
internal static string About_License_P1 => Get(nameof(About_License_P1));
internal static string About_License_P2 => Get(nameof(About_License_P2));
internal static string About_License_P3 => Get(nameof(About_License_P3));
internal static string About_SE_Heading => Get(nameof(About_SE_Heading));
internal static string About_SE_P1 => Get(nameof(About_SE_P1));
internal static string About_SE_P2 => Get(nameof(About_SE_P2));
internal static string About_Localization_Heading => Get(nameof(About_Localization_Heading));
internal static string About_Localization_P1 => Get(nameof(About_Localization_P1));
internal static string About_Localization_P2 => Get(nameof(About_Localization_P2));
internal static string About_Translators_TreeNode => Get(nameof(About_Translators_TreeNode));
// Hellion Chat — Auto-Tell-Tabs runtime strings
internal static string AutoTellTabs_Migration_Title => Get(nameof(AutoTellTabs_Migration_Title));
internal static string AutoTellTabs_Migration_Content => Get(nameof(AutoTellTabs_Migration_Content));
internal static string AutoTellTabs_SectionHeader => Get(nameof(AutoTellTabs_SectionHeader));
internal static string AutoTellTabs_HistorySeparator => Get(nameof(AutoTellTabs_HistorySeparator));
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
internal static string ChatLog_AutoTellTabs_Enable_Name => Get(nameof(ChatLog_AutoTellTabs_Enable_Name));
internal static string ChatLog_AutoTellTabs_Enable_Description => Get(nameof(ChatLog_AutoTellTabs_Enable_Description));
internal static string ChatLog_AutoTellTabs_Limit_Name => Get(nameof(ChatLog_AutoTellTabs_Limit_Name));
internal static string ChatLog_AutoTellTabs_Limit_Description => Get(nameof(ChatLog_AutoTellTabs_Limit_Description));
internal static string ChatLog_AutoTellTabs_Compact_Name => Get(nameof(ChatLog_AutoTellTabs_Compact_Name));
internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description));
internal static string ChatLog_AutoTellTabs_GreetedToggle_Name => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Name));
internal static string ChatLog_AutoTellTabs_GreetedToggle_Description => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Description));
internal static string ChatLog_AutoTellTabs_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint));
internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint));
// Hellion Chat — Auto-Tell-Tabs Privacy settings tab
internal static string Privacy_AutoTellTabs_Section_Title => Get(nameof(Privacy_AutoTellTabs_Section_Title));
internal static string Privacy_AutoTellTabs_Preload_Name => Get(nameof(Privacy_AutoTellTabs_Preload_Name));
internal static string Privacy_AutoTellTabs_Preload_Description => Get(nameof(Privacy_AutoTellTabs_Preload_Description));
internal static string Privacy_AutoTellTabs_Preload_Hint => Get(nameof(Privacy_AutoTellTabs_Preload_Hint));
}
-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();
}
}
}
-154
View File
@@ -1,154 +0,0 @@
using System.Numerics;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui;
internal class Popout : Window
{
private readonly ChatLogWindow ChatLogWindow;
private readonly Tab Tab;
private readonly int Idx;
private long FrameTime; // set every frame
private long LastActivityTime = Environment.TickCount64;
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx) : base($"{tab.Name}##popout")
{
ChatLogWindow = chatLogWindow;
Tab = tab;
Idx = idx;
Size = new Vector2(350, 350);
SizeCondition = ImGuiCond.FirstUseEver;
IsOpen = true;
RespectCloseHotkey = false;
DisableWindowSounds = true;
}
public override void PreOpenCheck()
{
if (!Tab.PopOut)
IsOpen = false;
}
public override bool DrawConditions()
{
FrameTime = Environment.TickCount64;
if (Tab.IndependentHide ? HideStateCheck() : ChatLogWindow.IsHidden)
return false;
if (!Plugin.Config.HideWhenInactive || (!Plugin.Config.InactivityHideActiveDuringBattle && Plugin.InBattle) || !Tab.UnhideOnActivity)
{
LastActivityTime = FrameTime;
return true;
}
// Activity in the tab, this popout window, or the main chat log window.
var lastActivityTime = Math.Max(Tab.LastActivity, LastActivityTime);
lastActivityTime = Math.Max(lastActivityTime, ChatLogWindow.LastActivityTime);
return FrameTime - lastActivityTime <= 1000 * Plugin.Config.InactivityHideTimeout;
}
public override void PreDraw()
{
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Push();
Flags = ImGuiWindowFlags.None;
if (!Plugin.Config.ShowPopOutTitleBar)
Flags |= ImGuiWindowFlags.NoTitleBar;
if (!Tab.CanMove)
Flags |= ImGuiWindowFlags.NoMove;
if (!Tab.CanResize)
Flags |= ImGuiWindowFlags.NoResize;
if (!ChatLogWindow.PopOutDocked[Idx])
{
var alpha = Tab.IndependentOpacity ? Tab.Opacity : Plugin.Config.WindowAlpha;
BgAlpha = alpha / 100f;
}
}
public override void Draw()
{
using var id = ImRaii.PushId($"popout-{Tab.Identifier}");
if (!Plugin.Config.ShowPopOutTitleBar)
{
ImGui.TextUnformatted(Tab.Name);
ImGui.Separator();
}
var handler = ChatLogWindow.HandlerLender.Borrow();
ChatLogWindow.DrawMessageLog(Tab, handler, ImGui.GetContentRegionAvail().Y, false);
if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows))
LastActivityTime = FrameTime;
}
public override void PostDraw()
{
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
}
public override void OnClose()
{
ChatLogWindow.PopOutWindows.Remove(Tab.Identifier);
ChatLogWindow.Plugin.WindowSystem.RemoveWindow(this);
Tab.PopOut = false;
ChatLogWindow.Plugin.SaveConfig();
}
private enum HideState
{
None,
Cutscene,
CutsceneOverride,
User,
Battle
}
private HideState CurrentHideState = HideState.None;
private bool HideStateCheck()
{
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
CurrentHideState = HideState.Battle;
// If the chat is hidden because of battle, we reset it here
if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
CurrentHideState = HideState.None;
// if the chat has no hide state and in a cutscene, set the hide state to cutscene
if (Tab.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
{
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
CurrentHideState = HideState.Cutscene;
}
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none
if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
CurrentHideState = HideState.None;
// if the chat is hidden because of a cutscene and the chat has been activated, show chat
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
CurrentHideState = HideState.CutsceneOverride;
// if the user hid the chat and is now activating chat, reset the hide state
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
CurrentHideState = HideState.None;
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn);
}
}
-186
View File
@@ -1,186 +0,0 @@
using System.Numerics;
using ChatTwo.Resources;
using ChatTwo.Ui.SettingsTabs;
using ChatTwo.Util;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing;
using Dalamud.Utility;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui;
public sealed class SettingsWindow : Window
{
private readonly Plugin Plugin;
private Configuration Mutable { get; }
private List<ISettingsTab> Tabs { get; }
private int CurrentTab;
internal SettingsWindow(Plugin plugin) : base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
{
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
SizeCondition = ImGuiCond.FirstUseEver;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(475, 600),
MaximumSize = new Vector2(float.MaxValue, float.MaxValue)
};
Plugin = plugin;
Mutable = new Configuration();
Tabs =
[
new Display(Mutable),
new ChatLog(Plugin, Mutable),
new Emote(Plugin, Mutable),
new Preview(Mutable),
new Fonts(Mutable),
new ChatColours(Plugin, Mutable),
new Tabs(Plugin, Mutable),
new SettingsTabs.Privacy(Plugin, Mutable),
new Database(Plugin, Mutable),
new Miscellaneous(Mutable),
new Changelog(Mutable),
new About()
];
RespectCloseHotkey = false;
DisableWindowSounds = true;
Initialise();
Plugin.Commands.Register("/hellion", "Perform various actions with Hellion Chat.").Execute += Command;
Plugin.Interface.UiBuilder.OpenConfigUi += Toggle;
}
public void Dispose()
{
Plugin.Interface.UiBuilder.OpenConfigUi -= Toggle;
Plugin.Commands.Register("/hellion").Execute -= Command;
}
private void Command(string command, string args)
{
if (string.IsNullOrWhiteSpace(args))
Toggle();
}
private void Initialise()
{
Mutable.UpdateFrom(Plugin.Config, false);
}
public override void Draw()
{
if (ImGui.IsWindowAppearing())
Initialise();
using (var table = ImRaii.Table("##chat2-settings-table", 2))
{
if (table.Success)
{
ImGui.TableSetupColumn("tab", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("settings", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
var changed = false;
for (var i = 0; i < Tabs.Count; i++)
{
if (!ImGui.Selectable($"{Tabs[i].Name}###tab-{i}", CurrentTab == i))
continue;
CurrentTab = i;
changed = true;
}
ImGui.TableNextColumn();
var style = ImGui.GetStyle();
var height = ImGui.GetContentRegionAvail().Y - style.FramePadding.Y * 2 - style.ItemSpacing.Y - style.ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y;
using var child = ImRaii.Child("##chat2-settings", new Vector2(-1, height));
if (child.Success)
Tabs[CurrentTab].Draw(changed);
}
}
ImGui.Separator();
var save = ImGui.Button(Language.Settings_Save);
ImGui.SameLine();
if (ImGui.Button(Language.Settings_SaveAndClose)) {
save = true;
IsOpen = false;
}
ImGui.SameLine();
if (ImGui.Button(Language.Settings_Discard)) {
IsOpen = false;
}
const string buttonLabel = "Anna's Ko-fi";
const string buttonLabel2 = "Infi's Ko-fi";
using (ImRaii.PushColor(ImGuiCol.Button, ColourUtil.RgbaToAbgr(0xFF5E5BFF)))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ColourUtil.RgbaToAbgr(0xFF7775FF)))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ColourUtil.RgbaToAbgr(0xFF4542FF)))
using (ImRaii.PushColor(ImGuiCol.Text, 0xFFFFFFFF))
{
var buttonWidth = ImGui.CalcTextSize(buttonLabel).X + ImGui.GetStyle().FramePadding.X * 2;
var buttonWidth2 = ImGui.CalcTextSize(buttonLabel2).X + ImGui.GetStyle().FramePadding.X * 2;
ImGui.SameLine(ImGui.GetContentRegionAvail().X - buttonWidth - buttonWidth2);
if (ImGui.Button(buttonLabel2))
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/infiii");
ImGui.SameLine();
if (ImGui.Button(buttonLabel))
Dalamud.Utility.Util.OpenLink("https://ko-fi.com/lojewalo");
}
if (!save)
return;
// calculate all conditions before updating config
var hideChanged = !Mutable.HideChat && Mutable.HideChat != Plugin.Config.HideChat;
var languageChanged = Mutable.LanguageOverride != Plugin.Config.LanguageOverride;
var fontChanged = Mutable.GlobalFontV2 != Plugin.Config.GlobalFontV2
|| Mutable.JapaneseFontV2 != Plugin.Config.JapaneseFontV2
|| Mutable.ItalicFontV2 != Plugin.Config.ItalicFontV2
|| Mutable.ExtraGlyphRanges != Plugin.Config.ExtraGlyphRanges
|| Mutable.UseHellionFont != Plugin.Config.UseHellionFont;
var fontSizeChanged = Math.Abs(Mutable.SymbolsFontSizeV2 - Plugin.Config.SymbolsFontSizeV2) > 0.001
|| Math.Abs(Mutable.FontSizeV2 - Plugin.Config.FontSizeV2) > 0.001;
var italicStateChanged = Mutable.ItalicEnabled != Plugin.Config.ItalicEnabled;
Plugin.Config.UpdateFrom(Mutable, true);
// save after 60 frames have passed, which should hopefully not
// commit any changes that cause a crash
Plugin.DeferredSaveFrames = 60;
Plugin.MessageManager.ClearAllTabs();
Plugin.MessageManager.FilterAllTabsAsync();
if (fontChanged || fontSizeChanged || italicStateChanged)
Plugin.FontManager.BuildFonts();
if (languageChanged)
Plugin.LanguageChanged(Plugin.Interface.UiLanguage);
if (hideChanged)
GameFunctions.GameFunctions.SetChatInteractable(true);
if (Plugin.Config.ShowEmotes)
Task.Run(EmoteCache.LoadData);
Initialise();
}
}
-128
View File
@@ -1,128 +0,0 @@
using System.Numerics;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class About : ISettingsTab
{
public string Name => string.Format(Language.Options_About_Tab, Plugin.PluginName) + "###tabs-about";
private readonly List<string> Translators =
[
"q673135110", "Akizem", "d0tiKs",
"Moonlight_Everlit", "Dark32", "andreycout",
"Button_", "Cali666", "cassandra308",
"lokinmodar", "jtabox", "AkiraYorumoto",
"MKhayle", "elena.space", "imlisa",
"andrei5125", "ShivaMaheshvara", "aislinn87",
"nishinatsu051", "lichuyuan", "Risu64",
"yummypillow", "witchymary", "Yuzumi",
"zomsakura", "Sirayuki"
];
internal About()
{
Translators.Sort((a, b) => string.Compare(a.ToLowerInvariant(), b.ToLowerInvariant(), StringComparison.Ordinal));
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGui.TextUnformatted(string.Format(Language.Options_About_Opening, Plugin.PluginName));
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextUnformatted(Language.Options_About_Authors);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGold, Plugin.Interface.Manifest.Author);
ImGui.TextUnformatted(Language.Options_About_Discord);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedGold, "@j.j_kazama");
ImGui.TextUnformatted(Language.Options_About_Version);
ImGui.SameLine();
ImGui.TextColored(ImGuiColors.ParsedOrange, Plugin.Interface.Manifest.AssemblyVersion.ToString(3));
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextUnformatted(Language.Options_About_Github_Issues);
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "githubIssues"))
Dalamud.Utility.Util.OpenLink("https://github.com/JonKazama-Hellion/HellionChat/issues");
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Maintainer_Heading);
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Body);
ImGui.TextUnformatted(HellionStrings.About_Maintainer_Website_Label);
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "hellionMedia"))
Dalamud.Utility.Util.OpenLink("https://hellion-media.de");
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Mission_Heading);
ImGui.TextUnformatted(HellionStrings.About_Mission_P1);
ImGui.Spacing();
ImGui.TextUnformatted(HellionStrings.About_Mission_P2);
ImGui.Spacing();
ImGui.TextUnformatted(HellionStrings.About_Mission_P3);
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_BuiltOn_Heading);
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_P1);
ImGui.Spacing();
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_P2);
ImGui.Spacing();
ImGui.TextUnformatted(HellionStrings.About_BuiltOn_Upstream_Label);
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.ExternalLinkAlt, "chatTwoUpstream"))
Dalamud.Utility.Util.OpenLink("https://github.com/Infiziert90/ChatTwo");
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_License_Heading);
ImGui.TextUnformatted(HellionStrings.About_License_P1);
ImGui.TextUnformatted(HellionStrings.About_License_P2);
ImGui.TextUnformatted(HellionStrings.About_License_P3);
ImGuiHelpers.ScaledDummy(10.0f);
ImGui.TextColored(ImGuiColors.DalamudOrange, HellionStrings.About_SE_Heading);
ImGui.TextUnformatted(HellionStrings.About_SE_P1);
ImGui.TextUnformatted(HellionStrings.About_SE_P2);
ImGui.Spacing();
ImGui.TextColored(ImGuiColors.ParsedGold, HellionStrings.About_Localization_Heading);
ImGui.TextUnformatted(HellionStrings.About_Localization_P1);
ImGui.TextUnformatted(HellionStrings.About_Localization_P2);
ImGui.Spacing();
// The translator list lives at the bottom of the About tab. Render
// it directly inside the parent scroll container instead of a
// fixed-height child — the previous "remaining space" calculation
// shrank to zero (or below) once the About copy grew, which made
// the section unreachable on smaller settings windows.
using (var treeNode = ImRaii.TreeNode(HellionStrings.About_Translators_TreeNode))
{
if (treeNode)
{
using var indent = ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false);
foreach (var translator in Translators)
ImGui.TextUnformatted(translator);
}
}
ImGui.Spacing();
}
}
-51
View File
@@ -1,51 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Changelog : ISettingsTab
{
private Configuration Mutable { get; }
public string Name => Language.Options_Changelog_Tab + "###tabs-changelog";
internal Changelog(Configuration mutable)
{
Mutable = mutable;
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGui.TextUnformatted(Language.Options_Warning_NotImplemented);
ImGuiUtil.OptionCheckbox(ref Mutable.PrintChangelog, Language.Options_PrintChangelog_Name, Language.Options_PrintChangelog_Description);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
var changelog = Plugin.Interface.Manifest.Changelog;
if (changelog != null)
{
ImGui.TextUnformatted(Language.Options_Changelog_Header);
ImGui.TextUnformatted($"Version {Plugin.Interface.Manifest.AssemblyVersion.ToString(3)}");
ImGui.Spacing();
foreach (var sentence in changelog.Split("\n"))
{
if (sentence == string.Empty)
{
ImGui.NewLine();
continue;
}
var condition = sentence.StartsWith('-') || sentence.StartsWith(" -");
using var indent = ImRaii.PushIndent(10.0f, true, condition);
ImGui.TextUnformatted(sentence);
}
}
ImGui.Spacing();
}
}
-69
View File
@@ -1,69 +0,0 @@
using ChatTwo.Code;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class ChatColours : ISettingsTab
{
private Plugin Plugin { get; }
private Configuration Mutable { get; }
public string Name => Language.Options_ChatColours_Tab + "###tabs-chat-colours";
internal ChatColours(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
#if DEBUG
// Users can set colours for ExtraChat linkshells in the ExtraChat plugin directly.
var sortable = ChatTypeExt.SortOrder
.SelectMany(entry => entry.Item2)
.Where(type => !type.IsGm() && !type.IsExtraChatLinkshell())
.ToHashSet();
var total = Enum.GetValues<ChatType>()
.Where(type => !type.IsGm() && !type.IsExtraChatLinkshell())
.ToHashSet();
if (sortable.Count != total.Count)
{
Plugin.Log.Warning($"There are {sortable.Count} sortable channels, but there are {total.Count} total channels.");
total.ExceptWith(sortable);
foreach (var missing in total)
Plugin.Log.Information($"Missing {missing}");
}
#endif
}
public void Draw(bool changed)
{
foreach (var (_, types) in ChatTypeExt.SortOrder)
{
foreach (var type in types)
{
if (ImGuiUtil.IconButton(FontAwesomeIcon.UndoAlt, $"{type}", Language.Options_ChatColours_Reset))
Mutable.ChatColours.Remove(type);
ImGui.SameLine();
if (ImGuiUtil.IconButton(FontAwesomeIcon.LongArrowAltDown, $"{type}", Language.Options_ChatColours_Import))
{
var gameColour = Plugin.Functions.Chat.GetChannelColor(type);
Mutable.ChatColours[type] = gameColour ?? type.DefaultColor() ?? 0;
}
ImGui.SameLine();
var vec = Mutable.ChatColours.TryGetValue(type, out var colour)
? ColourUtil.RgbaToVector3(colour)
: ColourUtil.RgbaToVector3(type.DefaultColor() ?? 0);
if (ImGui.ColorEdit3(type.Name(), ref vec, ImGuiColorEditFlags.NoInputs))
Mutable.ChatColours[type] = ColourUtil.Vector3ToRgba(vec);
}
}
ImGui.Spacing();
}
}
-159
View File
@@ -1,159 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface.Style;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class ChatLog : ISettingsTab
{
private readonly Plugin Plugin;
private Configuration Mutable { get; }
public string Name => Language.Options_ChatLog_Tab + "###tabs-chatlog";
internal ChatLog(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
}
public void Draw(bool changed)
{
using (ImRaii.TextWrapPos(0.0f))
{
ImGuiUtil.OptionCheckbox(ref Mutable.KeepInputFocus, Language.Options_KeepInputFocus_Name, Language.Options_KeepInputFocus_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.PlaySounds, Language.Options_PlaySounds_Name, Language.Options_PlaySounds_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.SidebarTabView, Language.Options_SidebarTabView_Name, string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.ShowNoviceNetwork, Language.Options_ShowNoviceNetwork_Name, Language.Options_ShowNoviceNetwork_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.ShowHideButton, Language.Options_ShowHideButton_Name, Language.Options_ShowHideButton_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.NativeItemTooltips, Language.Options_NativeItemTooltips_Name, string.Format(Language.Options_NativeItemTooltips_Description, Plugin.PluginName));
ImGui.Spacing();
if (Mutable.NativeItemTooltips)
{
ImGuiUtil.DragFloatVertical(Language.Options_TooltipOffset_Name, Language.Options_TooltipOffset_Desc, ref Mutable.TooltipOffset, 1, 0f, 400f, $"{Mutable.TooltipOffset:N0}px", ImGuiSliderFlags.AlwaysClamp);
ImGui.Spacing();
}
ImGuiUtil.DragFloatVertical(Language.Options_WindowOpacity_Name, ref Mutable.WindowAlpha, .25f, 0f, 100f, $"{Mutable.WindowAlpha:N2}%%", ImGuiSliderFlags.AlwaysClamp);
ImGui.Spacing();
if (ImGuiUtil.InputIntVertical(Language.Options_MaxLinesToShow_Name, Language.Options_MaxLinesToShow_Description, ref Mutable.MaxLinesToRender))
Mutable.MaxLinesToRender = Math.Clamp(Mutable.MaxLinesToRender, 1, 10_000);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.CanMove, Language.Options_CanMove_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.CanResize, Language.Options_CanResize_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.ShowTitleBar, Language.Options_ShowTitleBar_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.ShowPopOutTitleBar, Language.Options_ShowPopOutTitleBar_Name);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.OverrideStyle, Language.Options_OverrideStyle_Name, Language.Options_OverrideStyle_Name_Desc);
ImGui.Spacing();
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_ChatTabForwardKeybind_Name);
ImGui.SetNextItemWidth(-1);
ImGuiUtil.KeybindInput("ChatTabForwardKeybind", ref Mutable.ChatTabForward);
ImGui.TextUnformatted(Language.Options_ChatTabBackwardKeybind_Name);
ImGui.SetNextItemWidth(-1);
ImGuiUtil.KeybindInput("ChatTabBackwardKeybind", ref Mutable.ChatTabBackward);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_AdjustPosition_Name);
ImGui.SetNextItemWidth(-1);
var pos = Plugin.ChatLogWindow.LastWindowPos;
if (ImGui.DragFloat2($"##{Language.Options_AdjustPosition_Name}", ref pos, 1, 0, float.MaxValue, "%.0fpx"))
Plugin.ChatLogWindow.Position = pos;
ImGuiUtil.WarningText(Language.Options_AdjustPosition_Warning);
ImGui.Spacing();
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
DrawAutoTellTabsSection();
}
if (!Mutable.OverrideStyle)
return;
var styles = StyleModel.GetConfiguredStyles();
if (styles == null)
{
ImGui.TextUnformatted(Language.Options_OverrideStyle_NotAvailable);
ImGui.Spacing();
return;
}
var currentStyle = Mutable.ChosenStyle ?? Language.Options_OverrideStyle_NotSelected;
using var combo = ImRaii.Combo(Language.Options_OverrideStyleDropdown_Name, currentStyle);
if (combo)
{
foreach (var style in styles)
if (ImGui.Selectable(style.Name, Mutable.ChosenStyle == style.Name))
Mutable.ChosenStyle = style.Name;
}
ImGui.Spacing();
}
private void DrawAutoTellTabsSection()
{
using var tree = ImRaii.TreeNode(HellionStrings.ChatLog_AutoTellTabs_Section_Title);
if (!tree.Success)
return;
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Enable_Name, ref Mutable.EnableAutoTellTabs);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Enable_Description);
ImGui.SetNextItemWidth(200f * ImGuiHelpers.GlobalScale);
var limit = Mutable.AutoTellTabsLimit;
if (ImGui.SliderInt(HellionStrings.ChatLog_AutoTellTabs_Limit_Name, ref limit, 1, 50))
{
Mutable.AutoTellTabsLimit = limit;
}
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Limit_Description);
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description);
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name, ref Mutable.AutoTellTabsShowGreetedToggle);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description);
ImGui.Spacing();
ImGuiUtil.HelpText(HellionStrings.ChatLog_AutoTellTabs_PreloadHint);
ImGui.Spacing();
ImGuiUtil.WarningText(HellionStrings.ChatLog_AutoTellTabs_ConflictHint);
}
}
}
-233
View File
@@ -1,233 +0,0 @@
using System.Diagnostics;
using ChatTwo.Code;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Database : ISettingsTab
{
private Plugin Plugin { get; }
private Configuration Mutable { get; }
public string Name => Language.Options_Database_Tab + "###tabs-database";
internal Database(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
}
private bool ShowAdvanced;
private long DatabaseLastRefreshTicks;
private long DatabaseSize;
private long DatabaseLogSize;
private int DatabaseMessageCount;
public void Draw(bool changed)
{
if (changed)
ShowAdvanced = ImGui.GetIO().KeyShift;
ImGuiUtil.OptionCheckbox(ref Mutable.DatabaseBattleMessages, Language.Options_DatabaseBattleMessages_Name, Language.Options_DatabaseBattleMessages_Description);
ImGui.Spacing();
if (ImGuiUtil.OptionCheckbox(ref Mutable.LoadPreviousSession, Language.Options_LoadPreviousSession_Name, Language.Options_LoadPreviousSession_Description))
if (Mutable.LoadPreviousSession)
Mutable.FilterIncludePreviousSessions = true;
ImGui.Spacing();
if (ImGuiUtil.OptionCheckbox(ref Mutable.FilterIncludePreviousSessions, Language.Options_FilterIncludePreviousSessions_Name, Language.Options_FilterIncludePreviousSessions_Description))
if (!Mutable.FilterIncludePreviousSessions)
Mutable.LoadPreviousSession = false;
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
var old = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db"));
var migratedOld = new FileInfo(Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db"));
if (old.Exists || migratedOld.Exists)
{
ImGui.TextUnformatted(Language.Options_Database_Old_Heading);
ImGui.Spacing();
if (ImGuiUtil.CtrlShiftButton(Language.Options_Database_Old_Delete, Language.Options_Database_Old_Delete_Tooltip))
{
try
{
if (old.Exists)
old.Delete();
else
migratedOld.Delete();
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Success, NotificationType.Success);
}
catch (Exception e)
{
Plugin.Log.Error(e, "Unable to delete old database");
WrapperUtil.AddNotification(Language.Options_Database_Old_Delete_Error, NotificationType.Error);
}
}
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
}
ImGui.TextUnformatted(Language.Options_Database_Metadata_Heading);
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{
// Refresh the database size and message count every 5 seconds to avoid
// constant stat calls and spamming the database.
if (DatabaseLastRefreshTicks + 5 * 1000 < Environment.TickCount64)
{
DatabaseSize = Plugin.MessageManager.Store.DatabaseSize();
DatabaseLogSize = Plugin.MessageManager.Store.DatabaseLogSize();
DatabaseMessageCount = Plugin.MessageManager.Store.MessageCount();
DatabaseLastRefreshTicks = Environment.TickCount64;
}
// Copy the directory path instead of the file path so people can
// paste it into their file explorer.
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Path, MessageManager.DatabasePath()));
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
var path = Path.GetDirectoryName(MessageManager.DatabasePath());
ImGui.SetClipboardText(path);
WrapperUtil.AddNotification(Language.Options_Database_Metadata_CopyConfigPathNotification, NotificationType.Info);
}
if (ImGui.IsItemHovered())
{
ImGui.SetMouseCursor(ImGuiMouseCursor.Hand);
ImGuiUtil.Tooltip(Language.Options_Database_Metadata_CopyConfigPath);
}
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Size, StringUtil.BytesToString(DatabaseSize)));
if (ImGui.IsItemHovered())
ImGuiUtil.Tooltip(StringUtil.BytesToString(DatabaseSize));
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_LogSize, StringUtil.BytesToString(DatabaseLogSize)));
if (ImGui.IsItemHovered())
ImGuiUtil.Tooltip(StringUtil.BytesToString(DatabaseLogSize));
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_MessageCount, DatabaseMessageCount));
if (ImGuiUtil.CtrlShiftButton(Language.Options_ClearDatabase_Button, Language.Options_ClearDatabase_Tooltip))
{
Plugin.Log.Warning("Clearing messages from database");
Plugin.MessageManager.Store.ClearMessages();
Plugin.MessageManager.ClearAllTabs();
// Refresh on next draw
DatabaseLastRefreshTicks = 0;
WrapperUtil.AddNotification(Language.Options_ClearDatabase_Success, NotificationType.Info);
}
}
ImGui.Spacing();
if (!ShowAdvanced)
return;
using var treeNode = ImRaii.TreeNode(Language.Options_Database_Advanced);
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGuiUtil.WarningText(Language.Options_Database_Advanced_Warning);
if (ImGuiUtil.CtrlShiftButton("Perform maintenance", "Ctrl+Shift: MessageManager.Store.PerformMaintenance()"))
Plugin.MessageManager.Store.PerformMaintenance();
if (ImGuiUtil.CtrlShiftButton("Reload messages from database", "Ctrl+Shift: MessageManager.FilterAllTabs()"))
{
Plugin.MessageManager.ClearAllTabs();
Plugin.MessageManager.FilterAllTabsAsync();
}
if (ImGuiUtil.CtrlShiftButton("Inject 10,000 messages", "Ctrl+Shift: creates 10,000 unique messages (async)"))
new Thread(() => InsertMessages(10_000)).Start();
ImGui.Spacing();
}
private void InsertMessages(int count)
{
Plugin.Log.Info($"Inserting {count} messages due to user request");
// Generate
var stopwatch = Stopwatch.StartNew();
var playerName = Plugin.PlayerState.CharacterName;
var worldId = Plugin.PlayerState.HomeWorld.ValueNullable?.RowId ?? 0;
var senderSource = new SeStringBuilder()
.AddText("<")
.Add(new PlayerPayload(playerName, worldId))
.AddText("Random Message")
.Add(RawPayload.LinkTerminator)
.AddText(">: ")
.Build();
var senderChunks = ChunkUtil.ToChunks(senderSource, ChunkSource.Sender, ChatType.Debug).ToList();
var messages = new List<Message>(count);
for (var i = 0; i < count; i++)
{
var contentSource = new SeStringBuilder()
.AddText("Random message payload - ")
.AddItalics(Guid.NewGuid().ToString())
.Build();
var contentChunks = ChunkUtil.ToChunks(contentSource, ChunkSource.Content, ChatType.Debug).ToList();
var chatCode = new ChatCode(XivChatType.Say, 0, 0);
messages.Add(new Message(
Guid.NewGuid(),
Plugin.MessageManager.CurrentContentId,
Plugin.MessageManager.CurrentContentId,
DateTimeOffset.UtcNow,
chatCode,
senderChunks,
contentChunks,
senderSource,
contentSource,
Guid.Empty
));
}
var elapsedTicks = stopwatch.ElapsedTicks;
stopwatch.Stop();
Plugin.Log.Info($"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)");
// Insert
stopwatch = Stopwatch.StartNew();
foreach (var message in messages)
Plugin.MessageManager.Store.UpsertMessage(message);
elapsedTicks = stopwatch.ElapsedTicks;
stopwatch.Stop();
Plugin.Log.Info($"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)");
// Clear tabs during framework frame
Plugin.Framework.Run(() =>
{
stopwatch = Stopwatch.StartNew();
Plugin.MessageManager.ClearAllTabs();
elapsedTicks = stopwatch.ElapsedTicks;
stopwatch.Stop();
Plugin.Log.Info($"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)");
}).Wait();
// Fetch and filter during framework frame
Plugin.Framework.Run(() =>
{
stopwatch = Stopwatch.StartNew();
// Intentionally synchronous
Plugin.MessageManager.FilterAllTabs();
elapsedTicks = stopwatch.ElapsedTicks;
stopwatch.Stop();
Plugin.Log.Info($"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)");
}).Wait();
}
}
-116
View File
@@ -1,116 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Display : ISettingsTab
{
private Configuration Mutable { get; }
public string Name => Language.Options_Display_Tab + "###tabs-display";
internal Display(Configuration mutable)
{
Mutable = mutable;
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGuiUtil.OptionCheckbox(ref Mutable.HideChat, Language.Options_HideChat_Name, Language.Options_HideChat_Description);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideDuringCutscenes, Language.Options_HideDuringCutscenes_Name, string.Format(Language.Options_HideDuringCutscenes_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenNotLoggedIn, Language.Options_HideWhenNotLoggedIn_Name, string.Format(Language.Options_HideWhenNotLoggedIn_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenUiHidden, Language.Options_HideWhenUiHidden_Name, string.Format(Language.Options_HideWhenUiHidden_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideInLoadingScreens, Language.Options_HideInLoadingScreens_Name, string.Format(Language.Options_HideInLoadingScreens_Description, Plugin.PluginName));
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideInBattle, Language.Options_HideInBattle_Name, Language.Options_HideInBattle_Description);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.HideWhenInactive, Language.Options_HideWhenInactive_Name, Language.Options_HideWhenInactive_Description);
ImGui.Spacing();
if (Mutable.HideWhenInactive)
{
using var _ = ImRaii.PushIndent();
ImGuiUtil.InputIntVertical(Language.Options_InactivityHideTimeout_Name,
Language.Options_InactivityHideTimeout_Description, ref Mutable.InactivityHideTimeout, 1, 10);
// Enforce a minimum of 2 seconds to avoid people soft locking
// themselves.
Mutable.InactivityHideTimeout = Math.Max(2, Mutable.InactivityHideTimeout);
ImGui.Spacing();
// This setting conflicts with HideInBattle, so it's disabled.
using (ImRaii.Disabled(Mutable.HideInBattle))
{
ImGuiUtil.OptionCheckbox(ref Mutable.InactivityHideActiveDuringBattle,
Language.Options_InactivityHideActiveDuringBattle_Name,
Language.Options_InactivityHideActiveDuringBattle_Description);
ImGui.Spacing();
}
using var channelTree = ImRaii.TreeNode(Language.Options_InactivityHideChannels_Name);
if (channelTree.Success)
{
if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_All_Label, Language.Options_InactivityHideChannels_Button_Tooltip))
{
Mutable.InactivityHideChannelsV2 = TabsUtil.AllChannels();
Mutable.InactivityHideExtraChatAll = true;
Mutable.InactivityHideExtraChatChannels = [];
}
ImGui.SameLine();
if (ImGuiUtil.CtrlShiftButton(Language.Options_InactivityHideChannels_None_Label, Language.Options_InactivityHideChannels_Button_Tooltip))
{
Mutable.InactivityHideChannelsV2 = [];
Mutable.InactivityHideExtraChatAll = false;
Mutable.InactivityHideExtraChatChannels = [];
}
ImGui.Spacing();
ImGuiUtil.ChannelSelector(Language.Options_Tabs_Channels, Mutable.InactivityHideChannelsV2);
ImGuiUtil.ExtraChatSelector(Language.Options_Tabs_ExtraChatChannels,
ref Mutable.InactivityHideExtraChatAll, Mutable.InactivityHideExtraChatChannels);
}
ImGui.Spacing();
}
ImGui.Separator();
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.Use24HourClock, Language.Options_Use24HourClock_Name, Language.Options_Use24HourClock_Description);
ImGuiUtil.OptionCheckbox(ref Mutable.PrettierTimestamps, Language.Options_PrettierTimestamps_Name, Language.Options_PrettierTimestamps_Description);
if (Mutable.PrettierTimestamps)
{
using var _ = ImRaii.PushIndent();
ImGuiUtil.OptionCheckbox(ref Mutable.MoreCompactPretty, Language.Options_MoreCompactPretty_Name, Language.Options_MoreCompactPretty_Description);
ImGuiUtil.OptionCheckbox(ref Mutable.HideSameTimestamps, Language.Options_HideSameTimestamps_Name, Language.Options_HideSameTimestamps_Description);
}
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.CollapseDuplicateMessages, Language.Options_CollapseDuplicateMessages_Name, Language.Options_CollapseDuplicateMessages_Description);
if (Mutable.CollapseDuplicateMessages)
{
using var _ = ImRaii.PushIndent();
ImGuiUtil.OptionCheckbox(ref Mutable.CollapseKeepUniqueLinks, Language.Options_CollapseDuplicateMsgUniqueLink_Name, Language.Options_CollapseDuplicateMsgUniqueLink_Description);
}
ImGui.Spacing();
}
}
-113
View File
@@ -1,113 +0,0 @@
using System.Numerics;
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Emote : ISettingsTab
{
private readonly Plugin Plugin;
private Configuration Mutable { get; }
public string Name => Language.Options_Emote_Tab + "###tabs-emote";
private static SearchSelector.SelectorPopupOptions? WordPopupOptions;
internal Emote(Plugin plugin, Configuration mutable)
{
Plugin = plugin;
Mutable = mutable;
WordPopupOptions = new SearchSelector.SelectorPopupOptions
{
FilteredSheet = EmoteCache.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)).ToArray()
};
}
private SearchSelector.SelectorPopupOptions RefillSheet()
{
return new SearchSelector.SelectorPopupOptions
{
FilteredSheet = EmoteCache.SortedCodeArray.Where(w => !Mutable.BlockedEmotes.Contains(w)).ToArray()
};
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGuiUtil.OptionCheckbox(ref Mutable.ShowEmotes, Language.Options_ShowEmotes_Name, Language.Options_ShowEmotes_Desc);
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_Emote_BlockedEmotes);
ImGui.Spacing();
WordPopupOptions ??= RefillSheet();
if (EmoteCache.State is EmoteCache.LoadingState.Done && WordPopupOptions.FilteredSheet.Length == 0)
WordPopupOptions = RefillSheet();
var buttonWidth = ImGui.GetContentRegionAvail().X / 3;
using (Plugin.FontManager.FontAwesome.Push())
ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0));
if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions))
Mutable.BlockedEmotes.Add(newWord);
using(var table = ImRaii.Table("##BlockedWords", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
{
if (table)
{
ImGui.TableSetupColumn(Language.Options_Emote_EmoteTable);
ImGui.TableSetupColumn("##Del", ImGuiTableColumnFlags.WidthStretch, 0.07f);
ImGui.TableHeadersRow();
var copiedList = Mutable.BlockedEmotes.ToArray();
foreach (var word in copiedList)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(word);
ImGui.TableNextColumn();
if (ImGuiUtil.Button($"##{word}Del", FontAwesomeIcon.Trash, !ImGui.GetIO().KeyCtrl))
Mutable.BlockedEmotes.Remove(word);
}
}
}
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.TextUnformatted(Language.Options_Emote_EmoteStats);
ImGui.Spacing();
if (EmoteCache.State is EmoteCache.LoadingState.Done)
ImGui.TextColored(ImGuiColors.HealerGreen, Language.Options_Emote_Ready);
else
ImGui.TextColored(ImGuiColors.DPSRed, Language.Options_Emote_NotReady);
ImGui.TextUnformatted($"{Language.Options_Emote_Loaded} {EmoteCache.SortedCodeArray.Length}");
using (var emoteTable = ImRaii.Table("##LoadedEmotes", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner))
{
if (emoteTable)
{
ImGui.TableSetupColumn("##word1");
ImGui.TableSetupColumn("##word2");
ImGui.TableSetupColumn("##word3");
ImGui.TableSetupColumn("##word4");
ImGui.TableSetupColumn("##word5");
foreach (var word in EmoteCache.SortedCodeArray)
{
ImGui.TableNextColumn();
ImGui.TextUnformatted(word);
}
}
}
}
}
-97
View File
@@ -1,97 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud;
using Dalamud.Interface.FontIdentifier;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
namespace ChatTwo.Ui.SettingsTabs;
public class Fonts : ISettingsTab
{
private Configuration Mutable { get; }
public string Name => Language.Options_Fonts_Tab + "###tabs-fonts";
internal Fonts(Configuration mutable)
{
Mutable = mutable;
}
public void Draw(bool _)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
ImGui.Checkbox(Language.Options_FontsEnabled, ref Mutable.FontsEnabled);
ImGui.Spacing();
if (!Mutable.FontsEnabled)
{
ImGuiUtil.FontSizeCombo(Language.Options_FontSize_Name, ref Mutable.FontSizeV2);
}
else
{
var globalChooser = ImGuiUtil.FontChooser(Language.Options_Font_Name, Mutable.GlobalFontV2, false, ref _);
globalChooser?.ResultTask.ContinueWith(r =>
{
if (r.IsCompletedSuccessfully)
Mutable.GlobalFontV2 = r.Result;
});
ImGui.SameLine();
if (ImGui.Button("Reset##global"))
Mutable.GlobalFontV2 = new SingleFontSpec{ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
ImGuiUtil.HelpText(string.Format(Language.Options_Font_Description, Plugin.PluginName));
ImGuiUtil.WarningText(Language.Options_Font_Warning);
ImGui.Spacing();
// LocaleNames being null means it is likely a game font which all support JP symbols
var japaneseChooser = ImGuiUtil.FontChooser(Language.Options_JapaneseFont_Name, Mutable.JapaneseFontV2, false, ref _, id => !id.LocaleNames?.ContainsKey("ja-jp") ?? false, "いろはにほへと ちりぬるを");
japaneseChooser?.ResultTask.ContinueWith(r =>
{
if (r.IsCompletedSuccessfully)
Mutable.JapaneseFontV2 = r.Result;
});
ImGui.SameLine();
if (ImGui.Button("Reset##japanese"))
Mutable.JapaneseFontV2 = new SingleFontSpec{ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkMedium), SizePt = 12.75f };
ImGuiUtil.HelpText(string.Format(Language.Options_JapaneseFont_Description, Plugin.PluginName));
ImGui.Spacing();
var italicChooser = ImGuiUtil.FontChooser(Language.Options_ItalicFont_Name, Mutable.ItalicFontV2, true, ref Mutable.ItalicEnabled);
italicChooser?.ResultTask.ContinueWith(r =>
{
if (r.IsCompletedSuccessfully)
Mutable.ItalicFontV2 = r.Result;
});
ImGui.SameLine();
if (ImGui.Button("Reset##italic"))
{
Mutable.ItalicEnabled = false;
Mutable.ItalicFontV2 = new SingleFontSpec{ FontId = new DalamudAssetFontAndFamilyId(DalamudAsset.NotoSansCjkRegular), SizePt = 12.75f };
}
ImGuiUtil.HelpText(string.Format(Language.Options_Italic_Description, Plugin.PluginName));
ImGui.Spacing();
if (ImGui.CollapsingHeader(Language.Options_ExtraGlyphs_Name))
{
ImGuiUtil.HelpText(string.Format(Language.Options_ExtraGlyphs_Description, Plugin.PluginName));
var range = (int) Mutable.ExtraGlyphRanges;
foreach (var extra in Enum.GetValues<ExtraGlyphRanges>())
ImGui.CheckboxFlags(extra.Name(), ref range, (int) extra);
Mutable.ExtraGlyphRanges = (ExtraGlyphRanges) range;
}
ImGui.Spacing();
}
ImGuiUtil.FontSizeCombo(Language.Options_SymbolsFontSize_Name, ref Mutable.SymbolsFontSizeV2);
ImGuiUtil.HelpText(Language.Options_SymbolsFontSize_Description);
ImGui.Spacing();
}
}
-62
View File
@@ -1,62 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Miscellaneous(Configuration mutable) : ISettingsTab
{
private Configuration Mutable { get; } = mutable;
public string Name => Language.Options_Miscellaneous_Tab + "###tabs-miscellaneous";
public void Draw(bool changed)
{
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Language_Name, Mutable.LanguageOverride.Name()))
{
if (combo.Success)
{
foreach (var language in Enum.GetValues<LanguageOverride>())
if (ImGui.Selectable(language.Name()))
Mutable.LanguageOverride = language;
}
}
ImGuiUtil.HelpText(string.Format(Language.Options_Language_Description, Plugin.PluginName));
ImGui.Spacing();
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_CommandHelpSide_Name, Mutable.CommandHelpSide.Name()))
{
if (combo.Success)
{
foreach (var side in Enum.GetValues<CommandHelpSide>())
if (ImGui.Selectable(side.Name(), Mutable.CommandHelpSide == side))
Mutable.CommandHelpSide = side;
}
}
ImGuiUtil.HelpText(string.Format(Language.Options_CommandHelpSide_Description, Plugin.PluginName));
ImGui.Spacing();
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_KeybindMode_Name, Mutable.KeybindMode.Name()))
{
if (combo.Success)
{
foreach (var mode in Enum.GetValues<KeybindMode>())
{
if (ImGui.Selectable(mode.Name(), Mutable.KeybindMode == mode))
Mutable.KeybindMode = mode;
if (ImGui.IsItemHovered())
ImGuiUtil.Tooltip(mode.Tooltip() ?? "");
}
}
}
ImGuiUtil.HelpText(string.Format(Language.Options_KeybindMode_Description, Plugin.PluginName));
ImGui.Spacing();
ImGui.Checkbox(Language.Options_SortAutoTranslate_Name, ref Mutable.SortAutoTranslate);
ImGuiUtil.HelpText(Language.Options_SortAutoTranslate_Description);
ImGui.Spacing();
}
}
-42
View File
@@ -1,42 +0,0 @@
using ChatTwo.Resources;
using ChatTwo.Util;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
namespace ChatTwo.Ui.SettingsTabs;
internal sealed class Preview : ISettingsTab
{
private Configuration Mutable { get; }
public string Name => $"{Language.Options_Preview_Tab}###tabs-preview";
internal Preview(Configuration mutable)
{
Mutable = mutable;
}
public void Draw(bool changed)
{
using var wrap = ImRaii.TextWrapPos(0.0f);
using (var combo = ImGuiUtil.BeginComboVertical(Language.Options_Preview_Name, Mutable.PreviewPosition.Name()))
{
if (combo)
{
foreach (var position in Enum.GetValues<PreviewPosition>())
if (ImGui.Selectable(position.Name(), Mutable.PreviewPosition == position))
Mutable.PreviewPosition = position;
}
}
ImGuiUtil.HelpText(Language.Options_Preview_Description);
ImGui.Spacing();
if (ImGuiUtil.InputIntVertical(Language.Options_PreviewMinimum_Name, Language.Options_PreviewMinimum_Description, ref Mutable.PreviewMinimum))
Mutable.PreviewMinimum = Math.Clamp(Mutable.PreviewMinimum, 1, 250);
ImGui.Spacing();
ImGuiUtil.OptionCheckbox(ref Mutable.OnlyPreviewIf, Language.Options_PreviewOnlyIf_Name, Language.Options_PreviewOnlyIf_Description);
ImGui.Spacing();
}
}
-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.
//
@@ -183,6 +183,22 @@ internal sealed class AutoTellTabsService : IDisposable
return;
}
// v0.6.1 — if the victim is currently popped out, tear down the
// matching Popout window first. Otherwise the window stays in
// PopOutWindows + WindowSystem and renders empty / re-spawns on the
// next AddPopOutsToDraw tick. Latent since pop-outs were introduced;
// becomes visible with AutoTellTabsOpenAsPopout where dropping a
// popped tab is now a routine code path.
if (victim.Tab.PopOut)
{
var popout = _plugin.ChatLogWindow.ActivePopouts
.FirstOrDefault(p => p.TabIdentifier == victim.Tab.Identifier);
if (popout != null)
{
popout.IsOpen = false;
}
}
Plugin.Config.Tabs.RemoveAt(victim.Index);
// Re-anchor the active tab so the user does not silently end up on
@@ -200,9 +216,24 @@ internal sealed class AutoTellTabsService : IDisposable
// Preload first so the tab opens with chronological history above
// the current message — and so a slow DB query never causes a
// visible "empty tab, then history pops in" effect on screen.
PreloadHistory(tab, partner.Name, partner.World);
// The current message is already persisted in the store by the
// time MessageProcessed fires (see MessageManager.cs: UpsertMessage
// runs before the event), so we have to exclude it explicitly to
// avoid the separator landing below the live tell.
PreloadHistory(tab, partner.Name, partner.World, currentMessage.Id);
tab.AddMessage(currentMessage, unread: true);
// Hellion Chat v0.6.1 — opt-in: open new /tell tabs directly as a
// pop-out window. Set BEFORE Tabs.Add so the next render-tick's
// AddPopOutsToDraw() sees PopOut=true and spawns the Popout window
// alongside the tab going into the list. No SaveConfig() because
// auto-tell tabs are IsTempTab (session-only, never persisted).
if (Plugin.Config.AutoTellTabsOpenAsPopout)
{
tab.PopOut = true;
}
Plugin.Config.Tabs.Add(tab);
}
@@ -238,7 +269,7 @@ internal sealed class AutoTellTabsService : IDisposable
return $"{playerName}@World{worldRowId}";
}
private void PreloadHistory(Tab tab, string senderName, uint senderWorld)
private void PreloadHistory(Tab tab, string senderName, uint senderWorld, Guid currentMessageId)
{
var preloadCount = Plugin.Config.AutoTellTabsHistoryPreload;
if (preloadCount <= 0)
@@ -248,13 +279,21 @@ internal sealed class AutoTellTabsService : IDisposable
try
{
// Pull one extra row because the live tell that triggered this
// spawn is already in the store and would otherwise eat one of
// the user's preload-budget slots.
var history = _store.GetTellHistoryWithSender(
_messageManager.CurrentContentId,
senderName,
senderWorld,
preloadCount);
preloadCount + 1);
if (history.Count == 0)
var historicMessages = history
.Where(m => m.Id != currentMessageId)
.Take(preloadCount)
.ToList();
if (historicMessages.Count == 0)
{
// No prior tells with this player — leave the tab to start
// empty so the user does not see a "history loaded" marker
@@ -265,7 +304,7 @@ internal sealed class AutoTellTabsService : IDisposable
// The history list is already oldest-first, so a plain AddPrune
// loop produces the chronological order the user expects to see
// when the tab opens.
foreach (var message in history)
foreach (var message in historicMessages)
{
tab.Messages.AddPrune(message, MessageManager.MessageDisplayLimit);
}
@@ -343,6 +382,27 @@ internal sealed class AutoTellTabsService : IDisposable
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab;
// v0.6.1 — symmetric to DropOldestTempTab cleanup: tear down any
// popped-out temp tab windows before removing the tabs themselves,
// otherwise PopOutWindows + WindowSystem keep ghost entries until
// the next plugin reload. Especially relevant once Auto-Pop-Out is
// enabled — every logout would otherwise leak as many ghosts as
// there were active /tell pop-outs.
var poppedTempTabIds = Plugin.Config.Tabs
.Where(t => t.IsTempTab && t.PopOut)
.Select(t => t.Identifier)
.ToList();
if (poppedTempTabIds.Count > 0)
{
var poppedSet = poppedTempTabIds.ToHashSet();
foreach (var popout in _plugin.ChatLogWindow.ActivePopouts
.Where(p => poppedSet.Contains(p.TabIdentifier))
.ToList())
{
popout.IsOpen = false;
}
}
Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
// Force a switch to tab 0 if the active tab was a temp tab OR
+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,29 @@ public class ConfigKeyBind
[Serializable]
public class Configuration : IPluginConfiguration
{
private const int LatestVersion = 9;
private const int LatestVersion = 16;
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;
// v1.2.1 — Default geflippt von false → true. Card-Rows-Layout aus
// v1.2.0 wurde als zu dicht empfunden; Single-Line `[HH:mm] Sender:
// Text` ist besser lesbar und platzsparender. Bestand-User mit aktiv
// false werden durch die v15→v16-Migration auf den neuen Default
// gehoben (Heuristik: wer in v1.2.0 false hatte, hatte den damals
// neu eingeführten Default — kaum jemand hat aktiv abgeschaltet).
public bool UseCompactDensity = true;
// 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;
@@ -67,15 +86,6 @@ public class Configuration : IPluginConfiguration
// ChatTwo users skip it because the v6→v7 migration sets the flag.
public bool FirstRunCompleted;
// 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.
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 ~92%.
public float HellionThemeWindowOpacity = 0.92f;
// Use the bundled Exo 2 font (OFL-1.1) for the regular plugin font
// instead of whatever GlobalFontV2.FontId points at. Default ON so a
// fresh install gets the Hellion typography out-of-the-box; flip OFF
@@ -101,6 +111,33 @@ public class Configuration : IPluginConfiguration
// want the auto tabs themselves without the extra UI affordance.
public bool AutoTellTabsShowGreetedToggle;
// Hellion Chat — One-Time-Hint-Banner that introduces the v0.6.0 pop-out
// input feature. Set to true once the user dismisses the banner from a
// pop-out window; never reset after that.
public bool SeenPopOutInputHint;
// Hellion Chat — v0.6.0 master switch for the pop-out input bar.
// Global on purpose: per-tab makes no sense for Auto-Tell-Tabs which
// are session-only and would force the user to re-enable it for every
// new conversation. Default flipped to ON in v0.6.1 (was OFF in v0.6.0)
// because tester feedback called the manual toggle "umständlich, wirkt
// unfertig". v11 → v12 migration applies the same flip to existing users.
public bool PopOutInputEnabled = true;
// Hellion Chat — v0.6.1 One-Time-Hint-Banner that introduces the
// chat-header pop-out toolbar button and reminds about the pop-out
// input default flip. Set to true once the user dismisses the banner
// from the main chat window; never reset after that.
public bool SeenPopOutHeaderHint;
// Hellion Chat — v0.6.1 opt-in: when true, AutoTellTabsService.SpawnTempTab
// sets tab.PopOut = true on every new auto-tell tab so the conversation
// pops out as its own window directly. Closing the pop-out returns the
// tab to the sidebar via the standard Popout.OnClose() flow. Default OFF
// because the existing sidebar workflow is what most users (especially
// club greeters tracking many parallel tells) expect by default.
public bool AutoTellTabsOpenAsPopout;
public int GetRetentionDays(ChatType type)
{
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
@@ -116,6 +153,11 @@ public class Configuration : IPluginConfiguration
public bool HideWhenUiHidden = true;
public bool HideInLoadingScreens;
public bool HideInBattle;
// v1.2.1 — Default geflippt false → true. Hellion-UI im NG+-Menü
// versteckt zu halten ist konsistent mit den anderen Hide-Defaults
// (Cutscenes, Logged-out, UI-Hidden) — UI-out-of-the-way bei Story-
// Sequenzen.
public bool HideInNewGamePlusMenu = true;
public bool HideWhenInactive;
public int InactivityHideTimeout = 10;
public bool InactivityHideActiveDuringBattle = true;
@@ -130,7 +172,10 @@ public class Configuration : IPluginConfiguration
public bool NativeItemTooltips = true;
public bool PrettierTimestamps = true;
public bool MoreCompactPretty;
public bool HideSameTimestamps;
// v1.2.1 — Default geflippt false → true. Wiederholte Zeitstempel
// innerhalb derselben Minute lesen sich als Rauschen; ein einziger
// Timestamp pro Minute reicht aus um die Konversation zu verorten.
public bool HideSameTimestamps = true;
public bool ShowNoviceNetwork;
// Hellion Chat — vertical sidebar tab layout reads better than the
// horizontal tab strip in the company of Auto-Tell-Tabs (a club
@@ -147,7 +192,7 @@ public class Configuration : IPluginConfiguration
public LanguageOverride LanguageOverride = LanguageOverride.None;
public bool CanMove = true;
public bool CanResize = true;
public bool ShowTitleBar;
public bool ShowTitleBar = true;
public bool ShowPopOutTitleBar = true;
public bool DatabaseBattleMessages;
public bool LoadPreviousSession;
@@ -157,8 +202,16 @@ public class Configuration : IPluginConfiguration
public bool CollapseKeepUniqueLinks;
public bool PlaySounds = true;
public bool KeepInputFocus = true;
public int MaxLinesToRender = 10_000; // 1-10000
public bool Use24HourClock;
// v1.2.1 — Default gesenkt 5000 → 2500. 5000 ist auf Mid-Range-
// Hardware bei langen Sessions spürbar langsamer (Card-Layout
// re-Layout pro Frame), 2500 deckt eine typische Stunde Chat ab
// und bleibt smooth. User die mehr brauchen können bis 10000 hoch.
public int MaxLinesToRender = 2_500; // 1-10000
// Default ON to match a German / European 24h locale. The
// ChatLogWindow.cs format-flip in v0.5.1 honours this strictly via
// CultureInfo.InvariantCulture so the result is consistent across
// host locales.
public bool Use24HourClock = true;
public bool ShowEmotes = true;
public HashSet<string> BlockedEmotes = [];
@@ -186,12 +239,24 @@ public class Configuration : IPluginConfiguration
};
public float TooltipOffset;
public float WindowAlpha = 100f;
public Dictionary<ChatType, uint> ChatColours = new();
public List<Tab> Tabs = [];
// v1.2.1 — Default-Chat-Farben sind das Hellion-Brand-Preset. Der
// First-Run-Wizard bietet keine Theme-/Preset-Wahl an, daher kriegen
// neue User die Hellion-Brand-Farben out-of-the-box (Cyan-Familie für
// Standard/Tell, Ember/Warning für laute Channels). Bestand-User mit
// leerem ChatColours-Dict werden durch die v15→v16-Migration auf das
// Preset gehoben; User die bereits Custom-Farben haben, bleiben.
public Dictionary<ChatType, uint> ChatColours = BuildDefaultChatColours();
public bool OverrideStyle;
public string? ChosenStyle;
private static Dictionary<ChatType, uint> BuildDefaultChatColours()
{
var defaults = new Dictionary<ChatType, uint>();
foreach (var (channel, colour) in HellionChat.Resources.ChatColourPresets.All["Hellion"].Colours)
defaults[channel] = colour;
return defaults;
}
public bool ColorSelectedInputChannelButton = true;
public List<Tab> Tabs = [];
public ConfigKeyBind? ChatTabForward;
public ConfigKeyBind? ChatTabBackward;
@@ -208,6 +273,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;
@@ -243,7 +309,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;
@@ -253,21 +322,42 @@ public class Configuration : IPluginConfiguration
ItalicFontV2 = other.ItalicFontV2;
SymbolsFontSizeV2 = other.SymbolsFontSizeV2;
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
// *this* configuration alive across an UpdateFrom so a settings
// save (or sidebar-mode toggle) does not silently destroy the
// user's open tell conversations. Persistent tabs from `other`
// still get the regular clone-replace treatment.
// user's open tell conversations.
//
// For persistent tabs we go through Tab.Clone() which intentionally
// does NOT copy the NonSerialized Messages list (avoids shared
// mutable state on disk-load). On a settings save that means the
// chat history for every persistent tab would be wiped — bug
// reported by Flo 2026-05-05. We work around it by capturing the
// live MessageList (and LastSendUnread counter) by Identifier
// before the replace, then restoring it onto the freshly cloned
// tabs whose Identifier survives Tab.Clone(). New tabs added in
// settings get a fresh empty MessageList; deleted tabs lose their
// history (intended).
var liveTempTabs = Tabs.Where(t => t.IsTempTab).ToList();
Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t => t.Clone()).ToList();
var livePersistentSession = Tabs
.Where(t => !t.IsTempTab)
.ToDictionary(t => t.Identifier, t => (t.Messages, t.LastSendUnread));
Tabs = other.Tabs.Where(t => !t.IsTempTab).Select(t =>
{
var clone = t.Clone();
if (livePersistentSession.TryGetValue(clone.Identifier, out var live))
{
clone.Messages = live.Messages;
clone.LastSendUnread = live.LastSendUnread;
}
return clone;
}).ToList();
Tabs.AddRange(liveTempTabs);
OverrideStyle = other.OverrideStyle;
ChosenStyle = other.ChosenStyle;
ChatTabForward = other.ChatTabForward;
ChatTabBackward = other.ChatTabBackward;
@@ -281,15 +371,24 @@ public class Configuration : IPluginConfiguration
RetentionLastRunAt = other.RetentionLastRunAt;
FirstRunCompleted = other.FirstRunCompleted;
HellionThemeEnabled = other.HellionThemeEnabled;
HellionThemeWindowOpacity = other.HellionThemeWindowOpacity;
UseHellionFont = other.UseHellionFont;
// v1.1.0 theme engine fields
Theme = other.Theme;
WindowOpacity = other.WindowOpacity;
ReduceMotion = other.ReduceMotion;
UseCompactDensity = other.UseCompactDensity;
EnableAutoTellTabs = other.EnableAutoTellTabs;
AutoTellTabsLimit = other.AutoTellTabsLimit;
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
SeenPopOutInputHint = other.SeenPopOutInputHint;
PopOutInputEnabled = other.PopOutInputEnabled;
SeenPopOutHeaderHint = other.SeenPopOutHeaderHint;
AutoTellTabsOpenAsPopout = other.AutoTellTabsOpenAsPopout;
}
}
@@ -325,6 +424,11 @@ public class Tab
{
public string Name = Language.Tab_DefaultName;
// v1.2.0 — optionaler FontAwesome-Glyph-Name. Null bedeutet:
// Default-Mapping aus TabIconMapping greift (basiert auf Tab-Name).
// User können hier per Settings → Tabs einen eigenen Glyph setzen.
public string? Icon = null;
[Obsolete("Removed in favor of SelectedChannels")]
public Dictionary<ChatType, ChatSource> ChatCodes = new();
@@ -529,6 +633,20 @@ public class Tab
}
}
/// <summary>
/// Aktuelle Anzahl der gespeicherten Messages. Lock-acquire pro Read
/// ist OK für 1×/sec Status-Bar-Polling (v1.2.0).
/// </summary>
public int Count
{
get
{
LockSlim.Wait(-1);
try { return Messages.Count; }
finally { LockSlim.Release(); }
}
}
/// <summary>
/// Returns an array copy of the message list for usage outside of main thread
/// </summary>
@@ -8,7 +8,7 @@ using Dalamud.Bindings.ImGui;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
namespace ChatTwo;
namespace HellionChat;
public static class EmoteCache
{
@@ -35,20 +35,20 @@ public static class EmoteCache
public Emote Emote { get; set; }
[JsonPropertyName("id")]
public string Id { get; set; }
public required string Id { get; set; }
}
[Serializable]
public struct Emote()
{
[JsonPropertyName("id")]
public string Id { get; set; }
public required string Id { get; set; }
[JsonPropertyName("code")]
public string Code { get; set; }
public required string Code { get; set; }
[JsonPropertyName("imageType")]
public string ImageType { get; set; }
public required string ImageType { get; set; }
}
public enum LoadingState
@@ -66,16 +66,29 @@ public static class EmoteCache
public static string[] SortedCodeArray = [];
public static async void LoadData()
// 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,14 +116,29 @@ 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
// the Emotes tab after the network recovers) can retry. Without
// this the State stays on Loading and the early-out at the top
// of LoadData blocks every further attempt until plugin reload.
State = LoadingState.Unloaded;
Plugin.Log.Error(ex, "BetterTTV cache wasn't initialized");
}
}
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();
}
@@ -166,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
@@ -183,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;
@@ -204,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;
@@ -274,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;
@@ -294,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
@@ -302,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
@@ -141,7 +120,16 @@ public class FontManager
e => e.OnPreBuild(
tk =>
{
var config = new SafeFontConfig {SizePt = Plugin.Config.GlobalFontV2.SizePt, GlyphRanges = Ranges};
// v1.2.0 — Bei aktiver Hellion-Schrift (Exo 2 ist Variable-Font)
// wird die User-Schriftgröße aus FontSizeV2 als SizePt angewendet.
// Der Bestand-Pfad nutzt weiter GlobalFontV2.SizePt aus dem
// Custom-Font-Stack. Ohne diese Verzweigung war FontSizeV2 bei
// UseHellionFont=true wirkungslos, was 4K-User mit größerer
// Skalierung blockierte (Settings → Erscheinungsbild → Schriftarten).
var basePt = Plugin.Config.UseHellionFont
? Plugin.Config.FontSizeV2
: Plugin.Config.GlobalFontV2.SizePt;
var config = new SafeFontConfig {SizePt = basePt, GlyphRanges = Ranges};
config.MergeFont = Plugin.Config.UseHellionFont
? tk.AddFontFromMemory(GetHellionFontBytes(), config, "Hellion-Exo2")
: AddFontWithFallback(tk, Plugin.Config.GlobalFontV2.FontId, config, "global");
@@ -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,
@@ -414,13 +414,13 @@ internal unsafe class KeybindManager : IDisposable {
if (ConfigKeybindPressed(source, Plugin.Config.ChatTabForward))
{
Plugin.KeyState[Plugin.Config.ChatTabForward!.Key] = false;
Plugin.ChatLogWindow.ChangeTabDelta(1);
DispatchTabDelta(1);
return;
}
if (ConfigKeybindPressed(source, Plugin.Config.ChatTabBackward))
{
Plugin.KeyState[Plugin.Config.ChatTabBackward!.Key] = false;
Plugin.ChatLogWindow.ChangeTabDelta(-1);
DispatchTabDelta(-1);
return;
}
@@ -465,6 +465,24 @@ internal unsafe class KeybindManager : IDisposable {
}
}
// v0.6.0 — central dispatch for ChatTabForward/Backward. If a pop-out
// window currently has its compact input focused, the keybind is
// forwarded into that pop-out's ChatInputBar so the user navigates
// tabs in the window they are typing in. Otherwise the main window
// handles it (= v0.5.x behavior).
private void DispatchTabDelta(int delta)
{
foreach (var popout in Plugin.ChatLogWindow.ActivePopouts)
{
if (popout.HasFocusedInputBar && popout.InputBar != null)
{
popout.InputBar.HandleKeybindForward(delta);
return;
}
}
Plugin.ChatLogWindow.ChangeTabDelta(delta);
}
private static Keybind GetKeybind(string id)
{
var outData = new FFXIVClientStructs.FFXIV.Client.System.Input.Keybind();
@@ -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;
+98
View File
@@ -0,0 +1,98 @@
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
<PropertyGroup>
<!-- Hellion Chat versioning runs separately from upstream Chat 2.
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>1.2.2</Version>
<ImplicitUsings>enable</ImplicitUsings>
<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>HellionChat</RootNamespace>
</PropertyGroup>
<ItemGroup>
<!-- 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.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>
<Compile Update="Resources\Language.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Language.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\Language.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Language.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<!-- HellionChat — Hellion-specific resource bundle (HellionStrings.resx
+ HellionStrings.<lang>.resx) is picked up automatically by the SDK
default include. Designer.cs is hand-maintained, no auto-gen needed. -->
<!-- Bundled Hellion font (Exo 2, OFL-1.1). Embedded as a manifest
resource with a fixed LogicalName so FontManager can pull the
bytes back at runtime via AddFontFromMemory. The OFL license
text travels with it inside the assembly to satisfy the
"license must be distributed with the font" clause. -->
<ItemGroup>
<EmbeddedResource Include="Resources\HellionFont.ttf">
<LogicalName>HellionFont.ttf</LogicalName>
</EmbeddedResource>
<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>
<!-- Plugin icon. Copy images/* into the build output so Dalamud
finds the icon next to the DLL, and let the SDK default
DalamudPackager pipeline include the same path in the
release ZIP. Earlier we shipped a custom DalamudPackager
targets override that explicitly set HandleImages and
ImagesPath; that override conflicted with the SDK 15
default and the resulting manifest carried no IconUrl.
Removed in v0.5.2. -->
<ItemGroup>
<None Include="images\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+435
View File
@@ -0,0 +1,435 @@
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.
v1.2.2 — Theme catalogue grown to nine built-in themes:
Hellion Arctic, Hellion Spectrum (CVD-safe Deuteran/Protan),
Chat 2 Klassik, Event Horizon, Moonlit Bloom, Mint Grove,
Night Blue, Indigo Violet, Forge Merchantman.
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.2.2 — Theme Expansion**
Four new built-in themes round out the picker. No engine changes,
no settings touched — just more colour options.
- **Night Blue** — Royal Blue on deep marine. Cool tech-dashboard
mood, distinct from the brand themes.
- **Indigo Violet** — Royal Violet on deep indigo with a turquoise-
mint counter for an aurora glitter feel. Sister to Event Horizon
but darker and denser; the turquoise accent keeps the two
distinguishable.
- **Forge Merchantman** — Patina bronze on workshop slate, warm
amber counter. Hellion Forge given a theme of its own — sister
to Hellion Arctic but greener and warmer instead of cold cyan.
- **Hellion Spectrum** — Deuteran/Protan-safe channel colours
using Wong/Okabe-Ito palette tones. Channel identity (Tell pink,
Yell yellow, Shout orange, Party blue, FC green) is preserved;
tones are chosen so each channel stays distinguishable under
red-green colour vision deficiency. Covers the ~99% of CVD cases
that are red-green.
No schema bump, no migration. Default theme is unchanged (Hellion
Arctic). Existing custom themes keep working.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 1.2.1 — Settings Cleanup**
Re-sorted the settings menu so related options live together. Card names
now describe their contents in plain words — "Theme & Layout", "Fonts &
Colours", "Data Management" — and each card has a short subtitle so you
don't have to guess where a setting lives. No new features, just
housekeeping.
Card changes:
- Theme & Layout (new) collects the theme picker, window frame style
(title bar, sidebar, hide button, pop-out title bar) and the timestamp
style options.
- Fonts & Colours (new) is the new home for font choice, font size and
per-channel chat colours.
- Data Management (new) is everything you do with stored messages:
retention windows, cleanup, export, the database viewer and the
advanced shift-click tools. All previously scattered between Privacy
and Database.
- Privacy is now focused on one job: the privacy filter.
- Chat absorbs the Auto-Tell-Tabs history preload slider that used to
live under Privacy.
- General groups the keybind mode under Input where it belongs.
Cleanup:
- Removed legacy "Style override" option and the unused style-name field
— both made obsolete by the Themes system in 1.1.0.
- Removed the legacy WindowAlpha slider; if you had it set, the value is
automatically migrated to Theme & Layout → Window Style → Window
Transparency.
- Removed the unused ShowThemeQuickPicker schema field.
Migration v15 → v16:
- A backup of your previous config is written to
pluginConfigs/HellionChat.json.pre-v16-backup before the schema change,
in case you want to roll back manually.
- All other settings are preserved unchanged.
- One-time toast on first start if you previously had Style override
enabled — it explains the change. Users who never touched that setting
see no toast.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 1.2.0 — Layout Refresh**
Second UI cycle: tab layouts modernised in both modes, a new
bottom status bar, card-rows as default message render, and
Auto-Tell tabs that you can finally tell apart at a glance.
Sidebar (icon-only, fixed 44 px):
- Tab name on hover-tooltip, vertical accent pill on the
active tab, child background no longer paints the top
padding area.
- Per-tab custom icons via Settings → Tabs.
- Auto-Tell tabs: each partner gets a hashed icon (envelope/
star/heart/bell/bookmark/flag/fire) plus hashed color
(12-color palette) — 84 distinct combinations.
- Pulsing red dot in the top-right of any tab with unread
messages, subtle 2-second sine pulse, respects
Configuration.ReduceMotion.
Top tabs:
- Accent underline pill on the active tab instead of the old
background fill. Icon prefixes were attempted but reverted
— Dalamud's default font atlas has no FontAwesome glyphs.
Bottom status bar (22 px, 1×/sec cached):
- Active channel with color dot, Privacy-First badge, tab +
message counters, auto-tell counter (hidden at zero),
plugin version (right-aligned, muted).
Message rendering:
- Card rows by default — sender header in channel color, body
on its own line, subtle border between cards.
- Compact-Density toggle in Appearance returns the classic
single-line `[HH:mm] Sender: Text` layout.
Bug fixes from in-game testing:
- Settings save no longer wipes chat history. Refilter cycle
only runs when filter-relevant settings actually changed
(privacy, channel selection); cosmetic changes leave the
chat intact. Persistent and Auto-Tell tabs both survive.
- Hellion Schrift (Exo 2) no longer blocks font-size
adjustment — 4K users can scale up properly.
- Sidebar buttons align with the first message row, status
bar version slot is no longer clipped.
Migration v14 → v15: legacy theme fields removed
(HellionThemeEnabled, HellionThemeWindowOpacity). All other
settings preserved.
Polish (lerps, theme crossfade, header quick-picker) follows
in v1.3.0.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**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
+51
View File
@@ -0,0 +1,51 @@
using System.Collections.Generic;
namespace HellionChat;
// Hellion Chat — v0.6.0 shared input history. Replaces the embedded
// ChatLogWindow.InputBacklog so that pop-out windows with their own
// ChatInputBar can navigate the same Up/Down history as the main window.
// Index semantics are kept identical to the v0.5.x InputBacklog:
// index 0 = oldest entry
// index Count - 1 = newest entry
// Push performs move-to-newest deduplication: existing entries are
// removed before the new one is appended at the end.
public static class InputHistoryService
{
private const int MaxSize = 30;
private static readonly List<string> _entries = new();
public static IReadOnlyList<string> Entries => _entries;
public static int Count => _entries.Count;
public static void Push(string entry)
{
if (string.IsNullOrWhiteSpace(entry))
return;
var trimmed = entry.Trim();
// Move-to-newest: existing entries are removed before the append
// so the same line typed twice does not occupy two history slots.
for (var i = 0; i < _entries.Count; i++)
{
if (_entries[i] == trimmed)
{
_entries.RemoveAt(i);
break;
}
}
_entries.Add(trimmed);
if (_entries.Count > MaxSize)
_entries.RemoveAt(0);
}
public static string? GetByCursor(int cursor)
{
if (cursor < 0 || cursor >= _entries.Count)
return null;
return _entries[cursor];
}
}
@@ -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)
@@ -141,7 +151,13 @@ internal class MessageManager : IAsyncDisposable
internal void ClearAllTabs()
{
foreach (var tab in Plugin.Config.Tabs)
// Hellion Chat — TempTabs haben keine DB-Persistenz (session-only,
// direkt vom AutoTellTabsService befüllt). Ein Clear+Refilter würde
// sie leer hinterlassen weil FilterAllTabs nichts aus der DB
// findet — Tells sind oft durch Privacy-Filter blockiert oder
// schlicht session-flüchtig. TempTabs vom Clear-Pfad ausschließen
// damit Settings-Save den Tell-Verlauf nicht zerstört.
foreach (var tab in Plugin.Config.Tabs.Where(t => !t.IsTempTab))
tab.Clear();
}
@@ -155,7 +171,11 @@ internal class MessageManager : IAsyncDisposable
// We store the pending messages to be added to the chat log in a
// temporary list, and apply them all at once after filtering.
var pendingTabs = Plugin.Config.Tabs.Select(tab => (tab, new List<Message>())).ToList();
// TempTabs werden ausgeschlossen — sie bleiben live-state aus dem
// AutoTellTabsService, ein DB-Refilter würde sie nur partial
// wiederherstellen falls Tells in DB liegen, oder leer lassen wenn
// Privacy-Filter sie blockiert hat.
var pendingTabs = Plugin.Config.Tabs.Where(t => !t.IsTempTab).Select(tab => (tab, new List<Message>())).ToList();
foreach (var message in messages)
foreach (var (_, pendingMessages) in pendingTabs.Where(ptab => ptab.Item1.Matches(message)))
pendingMessages.Add(message);
@@ -219,7 +239,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 +251,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
{
@@ -239,6 +239,9 @@ internal class MessageStore : IDisposable
private bool ColumnExists(string table, string column)
{
// PRAGMA does not accept SQLite parameter bindings. The table name is
// a compile-time constant fed in from internal call sites, so the
// interpolation cannot be reached from any user-controlled path.
using var cmd = Connection.CreateCommand();
cmd.CommandText = $"PRAGMA table_info({table});";
using var reader = cmd.ExecuteReader();
@@ -298,8 +301,10 @@ internal class MessageStore : IDisposable
{
Plugin.Log.Information($"Setting version {version}");
using var cmd = Connection.CreateCommand();
// Parameters aren't supported for PRAGMA queries, and you can't set the
// version with a pragma_ function.
// PRAGMA does not accept SQLite parameter bindings, and there is no
// pragma_ function variant that can set the version either. The
// version is a compile-time int from the migration sequence, never
// user input.
cmd.CommandText = $"PRAGMA user_version = {version};";
cmd.ExecuteNonQuery();
}
@@ -346,31 +351,44 @@ internal class MessageStore : IDisposable
throw new ArgumentOutOfRangeException(nameof(chatTypeDaysMap), "Negative retention is not allowed.");
var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var clauses = new List<string>();
foreach (var (type, days) in chatTypeDaysMap)
{
var cutoff = nowMs - days * 86400000L;
clauses.Add($"(ChatType = {type} AND Date < {cutoff})");
}
// Catch-all for channels without an explicit override. "0" is treated
// as "do not delete by default" — without an explicit user override,
// unmapped channels stay forever instead of getting wiped immediately.
if (defaultDays > 0)
{
var cutoff = nowMs - defaultDays * 86400000L;
var explicitTypes = chatTypeDaysMap.Count > 0
? string.Join(",", chatTypeDaysMap.Keys)
: "-1"; // empty list would produce invalid SQL
clauses.Add($"(ChatType NOT IN ({explicitTypes}) AND Date < {cutoff})");
}
if (clauses.Count == 0)
if (chatTypeDaysMap.Count == 0 && defaultDays <= 0)
return 0;
long deleted;
using (var cmd = Connection.CreateCommand())
{
var clauses = new List<string>();
var index = 0;
foreach (var (type, days) in chatTypeDaysMap)
{
var cutoff = nowMs - days * 86400000L;
var typeParam = $"$type{index}";
var cutoffParam = $"$cutoff{index}";
cmd.Parameters.AddWithValue(typeParam, type);
cmd.Parameters.AddWithValue(cutoffParam, cutoff);
clauses.Add($"(ChatType = {typeParam} AND Date < {cutoffParam})");
index++;
}
// Catch-all for channels without an explicit override. "0" is
// treated as "do not delete by default" — without an explicit
// user override, unmapped channels stay forever instead of
// getting wiped immediately.
if (defaultDays > 0)
{
var defaultCutoff = nowMs - defaultDays * 86400000L;
cmd.Parameters.AddWithValue("$defaultCutoff", defaultCutoff);
var explicitPlaceholders = chatTypeDaysMap.Count > 0
? BindIntList(cmd, "explicit", chatTypeDaysMap.Keys)
: "-1"; // empty list would produce invalid SQL
clauses.Add($"(ChatType NOT IN ({explicitPlaceholders}) AND Date < $defaultCutoff)");
}
if (clauses.Count == 0)
return 0;
cmd.CommandText = $"DELETE FROM messages WHERE {string.Join(" OR ", clauses)};";
cmd.CommandTimeout = 600;
deleted = cmd.ExecuteNonQuery();
@@ -395,11 +413,11 @@ internal class MessageStore : IDisposable
throw new InvalidOperationException("CleanupRetainOnly requires at least one allowed ChatType. Use ClearMessages for a full wipe.");
}
var inList = string.Join(",", allowedTypes);
long deleted;
using (var cmd = Connection.CreateCommand())
{
cmd.CommandText = $"DELETE FROM messages WHERE ChatType NOT IN ({inList});";
var placeholders = BindIntList(cmd, "ct", allowedTypes);
cmd.CommandText = $"DELETE FROM messages WHERE ChatType NOT IN ({placeholders});";
cmd.CommandTimeout = 600;
deleted = cmd.ExecuteNonQuery();
}
@@ -434,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;
}
@@ -512,15 +533,16 @@ internal class MessageStore : IDisposable
DateTimeOffset? from,
DateTimeOffset? to)
{
var cmd = Connection.CreateCommand();
var clauses = new List<string> { "deleted = false" };
if (chatTypes is { Count: > 0 })
clauses.Add($"ChatType IN ({string.Join(",", chatTypes)})");
clauses.Add($"ChatType IN ({BindIntList(cmd, "exct", chatTypes)})");
if (from is not null)
clauses.Add("Date >= $From");
if (to is not null)
clauses.Add("Date <= $To");
var cmd = Connection.CreateCommand();
cmd.CommandText = @"
SELECT
Id,
@@ -693,16 +715,17 @@ internal class MessageStore : IDisposable
internal long CountDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
{
using var cmd = Connection.CreateCommand();
List<string> whereClauses = ["deleted = false"];
if (receiver != null)
whereClauses.Add("Receiver = $Receiver");
whereClauses.Add("Date BETWEEN $After AND $Before");
whereClauses.Add($"ChatType IN ({string.Join(", ", channels)})");
whereClauses.Add($"ChatType IN ({BindIntList(cmd, "cdr", channels.Select(c => (int)c))})");
var whereClause = "WHERE " + string.Join(" AND ", whereClauses);
using var cmd = Connection.CreateCommand();
// Select last N messages by date DESC, but reverse the order to get
// them in ascending order.
cmd.CommandText = @"
@@ -722,16 +745,17 @@ internal class MessageStore : IDisposable
internal MessageEnumerator GetDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null)
{
var cmd = Connection.CreateCommand();
List<string> whereClauses = ["deleted = false"];
if (receiver != null)
whereClauses.Add("Receiver = $Receiver");
whereClauses.Add("Date BETWEEN $After AND $Before");
whereClauses.Add($"ChatType IN ({string.Join(", ", channels)})");
whereClauses.Add($"ChatType IN ({BindIntList(cmd, "gdr", channels.Select(c => (int)c))})");
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
var cmd = Connection.CreateCommand();
// Select last N messages by date DESC, but reverse the order to get
// them in ascending order.
cmd.CommandText = @"
@@ -763,16 +787,17 @@ internal class MessageStore : IDisposable
internal MessageEnumerator GetPagedDateRange(DateTime after, DateTime before, IEnumerable<byte> channels, ulong? receiver = null, int page = 0)
{
var cmd = Connection.CreateCommand();
List<string> whereClauses = ["deleted = false"];
if (receiver != null)
whereClauses.Add("Receiver = $Receiver");
whereClauses.Add("Date BETWEEN $After AND $Before");
whereClauses.Add($"ChatType IN ({string.Join(", ", channels)})");
whereClauses.Add($"ChatType IN ({BindIntList(cmd, "pdr", channels.Select(c => (int)c))})");
var whereClause = $"WHERE {string.Join(" AND ", whereClauses)}";
var cmd = Connection.CreateCommand();
// Select last N messages by date DESC, but reverse the order to get
// them in ascending order.
cmd.CommandText = @"
@@ -806,6 +831,24 @@ internal class MessageStore : IDisposable
return new MessageEnumerator(cmd.ExecuteReader());
}
// Build "$prefix0,$prefix1,..." placeholder list and bind values to
// the command. SQLite has no native array parameter, so we generate
// the list at runtime and bind each entry under its own name. Used
// for IN-clauses and similar dynamic-arity SQL fragments.
private static string BindIntList(SqliteCommand cmd, string prefix, IEnumerable<int> values)
{
var names = new List<string>();
var index = 0;
foreach (var value in values)
{
var name = $"${prefix}{index}";
cmd.Parameters.AddWithValue(name, value);
names.Add(name);
index++;
}
return string.Join(",", names);
}
}
internal class MessageEnumerator(DbDataReader reader) : IEnumerable<Message>, IDisposable, IAsyncDisposable
@@ -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()));
+349 -73
View File
@@ -1,10 +1,11 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using ChatTwo.Ipc;
using ChatTwo.Resources;
using ChatTwo.Ui;
using ChatTwo.Util;
using System.IO;
using HellionChat.Ipc;
using HellionChat.Resources;
using HellionChat.Ui;
using HellionChat.Util;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Windowing;
using Dalamud.IoC;
@@ -13,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
@@ -62,6 +63,8 @@ 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 Ui.StatusBar StatusBar { get; private set; } = null!;
internal int DeferredSaveFrames = -1;
@@ -71,8 +74,11 @@ public sealed class Plugin : IDalamudPlugin
// plugin start would launch two sweeps in parallel and the second one
// would just re-do work the first one already finished. The lock guards
// the flag — the flag check itself bails before we touch the database.
// Volatile because the ImGui thread reads the flag outside the lock to
// gate the manual button; without it the JIT may cache the value in a
// register and miss the background-thread update.
internal readonly object RetentionSweepLock = new();
internal bool RetentionSweepRunning;
internal volatile bool RetentionSweepRunning;
internal DateTime GameStarted { get; }
@@ -90,6 +96,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();
@@ -107,94 +119,304 @@ public sealed class Plugin : IDalamudPlugin
// Drop them on load to guarantee the session-only invariant.
Config.Tabs.RemoveAll(t => t.IsTempTab);
#pragma warning disable CS0618 // Type or member is obsolete
// TODO Remove after 01.07.2026
// Migrate old channel values
if (Config.Version <= 5)
// Hellion Chat v9 → v10 — wipes the configuration so the new 8-tab
// layout starts from defaults instead of mapping every previous setting
// to its new position. Backup-Failure ist non-fatal, der Wipe läuft
// trotzdem; dem User fehlt dann nur das manuelle Restore-Sicherheitsnetz.
if (Config.Version < 10)
{
foreach (var tab in Config.Tabs)
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
if (pluginConfigsDir is not null)
{
if (tab.ChatCodes.Count > 0)
{
tab.SelectedChannels = tab.ChatCodes.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value));
tab.ChatCodes.Clear();
}
var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v10-backup");
if (Config.InactivityHideChannels.Count > 0)
try
{
Config.InactivityHideChannelsV2 = Config.InactivityHideChannels.ToDictionary(pair => pair.Key, pair => (pair.Value, pair.Value));
Config.InactivityHideChannels.Clear();
if (File.Exists(liveConfigPath))
{
File.Copy(liveConfigPath, backupPath, overwrite: true);
}
}
catch (Exception ex)
{
Log.Warning(ex, "HellionChat: pre-v10 config backup failed");
}
Config.Version = 6;
SaveConfig();
}
}
#pragma warning restore CS0618 // Type or member is obsolete
// Hellion Chat v6→v7: seed Privacy-First defaults.
if (Config.Version <= 6)
{
Config.PrivacyFilterEnabled = true;
Config.PrivacyPersistChannels = [..Privacy.PrivacyDefaults.PrivacyFirstWhitelist];
Config.PrivacyPersistUnknownChannels = false;
// Existing ChatTwo users skip the first-run wizard — the
// migration toast already explains what changed and they
// can reopen the wizard from Settings → Privacy if they
// want to pick a different profile.
Config.FirstRunCompleted = true;
Config.Version = 7;
Config = new Configuration
{
Version = 10,
FirstRunCompleted = true,
};
SaveConfig();
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
{
Title = HellionStrings.Migration_Notification_Title,
Content = HellionStrings.Migration_Notification_Content,
Title = HellionStrings.SettingsRefactor_Migration_Title,
Content = HellionStrings.SettingsRefactor_Migration_Content,
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
InitialDuration = TimeSpan.FromSeconds(15),
InitialDuration = TimeSpan.FromSeconds(25),
});
}
// Hellion Chat v7→v8: webinterface removed in 0.2.0. Old config
// entries (WebinterfacePassword, AuthStore, etc.) get dropped on
// the next save because their properties no longer exist on the
// Configuration class. The bump is recorded so the notification
// only fires once.
if (Config.Version <= 7)
// Hellion Chat v10 → v11 — adds the global Configuration.PopOutInputEnabled
// master switch and SeenPopOutInputHint flag for the v0.6.0 pop-out
// input feature. Lightweight migration: defaults both fields,
// no user-facing notification because the change is opt-in only.
if (Config.Version < 11)
{
Config.Version = 8;
Config.PopOutInputEnabled = false;
Config.SeenPopOutInputHint = false;
Config.Version = 11;
SaveConfig();
Log.Information(
"Migrated config v10 → v11: PopOutInputEnabled added (global, default off), " +
"SeenPopOutInputHint added (default false)");
}
// Hellion Chat v11 → v12 — flips Configuration.PopOutInputEnabled from
// the v0.6.0 opt-in default (false) to opt-out (true) per v0.6.1 UX
// polish. Hard-flip is a deliberate design call (see Spec section 5.7);
// users are notified via the v0.6.1 hint banner (SeenPopOutHeaderHint
// reset). Re-toggle after migration is preserved because this block
// only fires for Version < 12.
if (Config.Version < 12)
{
Config.PopOutInputEnabled = true;
Config.SeenPopOutHeaderHint = false;
Config.Version = 12;
SaveConfig();
Log.Information(
"Migrated config v11 → v12: PopOutInputEnabled hard-flipped to true (v0.6.1 default), " +
"SeenPopOutHeaderHint reset to false (v0.6.1 banner re-armed)");
}
// 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.Migration_Webinterface_Removed_Title,
Content = HellionStrings.Migration_Webinterface_Removed_Content,
Title = HellionStrings.SettingsRefactor_Migration_Title,
Content = HellionStrings.SettingsRefactor_Migration_Content,
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
InitialDuration = TimeSpan.FromSeconds(20),
InitialDuration = TimeSpan.FromSeconds(25),
});
}
// Hellion Chat v8→v9: Auto-Tell-Tabs feature seeded with
// property-initializer defaults (enabled, limit 15, history 20,
// section header on). No data migration needed — just bump the
// version and notify the user once so the feature does not
// surprise them.
if (Config.Version <= 8)
// 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.Version = 9;
Config.Theme = "hellion-arctic";
// v1.2.0: alter Opacity-Wert wird nicht mehr migriert (Field entfernt).
// User die direkt v13 → v15 springen bekommen den Default 0.85.
Config.ReduceMotion = false;
Config.UseCompactDensity = false;
Config.Version = 14;
SaveConfig();
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
{
Title = HellionStrings.AutoTellTabs_Migration_Title,
Content = HellionStrings.AutoTellTabs_Migration_Content,
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
InitialDuration = TimeSpan.FromSeconds(20),
});
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");
}
if (Config.Version < 15)
{
// v1.2.0 — keine Datenmigration nötig. Removal der deprecated
// Theme-Felder ist reine Schema-Bereinigung (System.Text.Json
// ignoriert unbekannte Felder im JSON, daher kein Crash bei
// Configs die noch HellionThemeEnabled/HellionThemeWindowOpacity
// serialisiert haben — die Werte verfallen einfach).
Config.Version = 15;
SaveConfig();
Log.Information(
"Migrated config v14 → v15: legacy theme fields removed " +
"(HellionThemeEnabled, HellionThemeWindowOpacity)");
}
// Hellion Chat v15 → v16 — Settings Cleanup. Re-Sortierung der
// Tabs auf der UI-Seite (datenneutral). 4 tote Felder verfallen
// beim System.Text.Json-Deserialize (OverrideStyle, ChosenStyle,
// WindowAlpha, ShowThemeQuickPicker — sind alle nicht mehr im
// Configuration-Schema definiert). WindowAlpha wird zuvor auf
// WindowOpacity gemappt damit User die ihn gesetzt hatten ihre
// Transparenz-Einstellung behalten.
if (Config.Version < 16)
{
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
var liveConfigPath = pluginConfigsDir is not null
? Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json")
: null;
// Backup-Datei neben der live Config — Pattern aus v13 Branch.
if (pluginConfigsDir is not null && liveConfigPath is not null)
{
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v16-backup");
try
{
if (File.Exists(liveConfigPath))
File.Copy(liveConfigPath, backupPath, overwrite: true);
}
catch (Exception ex)
{
Log.Warning(ex, "HellionChat: pre-v16 config backup failed");
}
}
// Pre-v16 Felder einmalig roh aus dem JSON lesen, da sie nicht
// mehr im Configuration-Schema sind (und damit aus Config nicht
// mehr abrufbar). WindowAlpha → WindowOpacity Mapping nur wenn
// User WindowOpacity noch nicht selbst angefasst hat (Default
// 0.85), sonst gewinnt der User-Wert.
float oldWindowAlpha = 100f;
bool oldOverrideStyle = false;
if (liveConfigPath is not null)
{
try
{
if (File.Exists(liveConfigPath))
{
var rawJson = File.ReadAllText(liveConfigPath);
using var doc = System.Text.Json.JsonDocument.Parse(rawJson);
if (doc.RootElement.TryGetProperty("WindowAlpha", out var alphaProp)
&& alphaProp.ValueKind == System.Text.Json.JsonValueKind.Number)
{
oldWindowAlpha = alphaProp.GetSingle();
}
if (doc.RootElement.TryGetProperty("OverrideStyle", out var ovProp)
&& ovProp.ValueKind is System.Text.Json.JsonValueKind.True)
{
oldOverrideStyle = true;
}
}
}
catch (Exception ex)
{
Log.Warning(ex, "HellionChat: pre-v16 legacy-field lookup failed, defaults assumed");
}
}
if (oldWindowAlpha != 100f
&& Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f)
{
Config.WindowOpacity = Math.Clamp(oldWindowAlpha / 100f, 0.5f, 1.0f);
Log.Information(
$"Migrated WindowAlpha {oldWindowAlpha} to WindowOpacity {Config.WindowOpacity}");
}
else if (oldWindowAlpha != 100f)
{
Log.Information(
$"Skipped WindowAlpha→WindowOpacity migration: WindowOpacity already user-set " +
$"({Config.WindowOpacity}), legacy WindowAlpha value {oldWindowAlpha} dropped.");
}
if (oldOverrideStyle)
{
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
{
Title = "Hellion Chat 1.2.1",
Content = HellionStrings.Migration_v16_OverrideStyle_Toast,
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
InitialDuration = TimeSpan.FromSeconds(25),
});
}
// v1.2.1 Default-Bumps für UX-Verbesserungen. Pattern: nur
// migrieren wenn der User noch auf dem alten Default ist.
// Bei bool-Werten ist die Erkennung pragmatisch — wer den
// alten Default aktiv ausgeschaltet hatte, erlebt das als
// Regression und stellt es einmal in den Settings zurück.
// Der Trade-Off ist akzeptabel weil die alten Defaults in
// v1.2.0 erst neu eingeführt wurden und kaum jemand aktiv
// umgeschaltet hat.
if (!Config.UseCompactDensity)
{
Config.UseCompactDensity = true;
Log.Information("v16 default-bump: UseCompactDensity false → true");
}
if (!Config.HideInNewGamePlusMenu)
{
Config.HideInNewGamePlusMenu = true;
Log.Information("v16 default-bump: HideInNewGamePlusMenu false → true");
}
if (!Config.HideSameTimestamps)
{
Config.HideSameTimestamps = true;
Log.Information("v16 default-bump: HideSameTimestamps false → true");
}
if (Config.MaxLinesToRender == 5000)
{
Config.MaxLinesToRender = 2500;
Log.Information("v16 default-bump: MaxLinesToRender 5000 → 2500");
}
if (Config.ChatColours.Count == 0)
{
foreach (var (channel, colour) in Resources.ChatColourPresets.All["Hellion"].Colours)
Config.ChatColours[channel] = colour;
Log.Information("v16 default-bump: ChatColours empty → Hellion brand preset");
}
Config.Version = 16;
SaveConfig();
Log.Information(
"Migrated config v15 → v16: settings cleanup, " +
"OverrideStyle/ChosenStyle/WindowAlpha/ShowThemeQuickPicker dropped from schema");
}
// 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);
Config.Tabs.Add(TabsUtil.HellionLinkshell);
}
LanguageChanged(Interface.UiLanguage);
ImGuiUtil.Initialize(this);
@@ -208,6 +430,16 @@ 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);
StatusBar = new Ui.StatusBar();
MessageManager = new MessageManager(this); // Does it require UI?
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
@@ -267,7 +499,7 @@ public sealed class Plugin : IDalamudPlugin
Interface.UiBuilder.OpenMainUi += OpenMainUi;
if (Config.ShowEmotes)
Task.Run(EmoteCache.LoadData);
_ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
#if !DEBUG
// Avoid 300ms hitch when sending first message by preloading the
@@ -471,10 +703,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
@@ -496,13 +733,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();
@@ -513,6 +747,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;
@@ -577,4 +821,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
{
+316
View File
@@ -0,0 +1,316 @@
using System.Collections.Generic;
using HellionChat.Code;
using HellionChat.Util;
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
// settings UI which overwrites Configuration.ChatColours immediately.
// Battle-channel types are intentionally NOT covered by the stylistic
// presets so that combat-log tuning the user has done stays intact.
public sealed record ChatColourPreset(
string DisplayName,
string LocalizationKey,
bool IsBrandPreset,
IReadOnlyDictionary<ChatType, uint> Colours);
public static class ChatColourPresets
{
public static IReadOnlyDictionary<string, ChatColourPreset> All { get; } = BuildAll();
private static Dictionary<string, ChatColourPreset> BuildAll()
{
return new Dictionary<string, ChatColourPreset>
{
["Default"] = new(
DisplayName: "ChatTwo Default",
LocalizationKey: "ChatColourPresets_Default",
IsBrandPreset: false,
Colours: BuildDefault()),
["HighContrast"] = new(
DisplayName: "High-Contrast",
LocalizationKey: "ChatColourPresets_HighContrast",
IsBrandPreset: false,
Colours: BuildHighContrast()),
["Pastell"] = new(
DisplayName: "Pastell",
LocalizationKey: "ChatColourPresets_Pastell",
IsBrandPreset: false,
Colours: BuildPastell()),
["DarkModeTuned"] = new(
DisplayName: "Dark-Mode-Tuned",
LocalizationKey: "ChatColourPresets_DarkModeTuned",
IsBrandPreset: false,
Colours: BuildDarkModeTuned()),
["Hellion"] = new(
DisplayName: "Hellion",
LocalizationKey: "ChatColourPresets_Hellion",
IsBrandPreset: true,
Colours: BuildHellion()),
["NightBlue"] = new(
DisplayName: "Night Blue",
LocalizationKey: "ChatColourPresets_NightBlue",
IsBrandPreset: false,
Colours: BuildNightBlue()),
["IndigoViolet"] = new(
DisplayName: "Indigo Violet",
LocalizationKey: "ChatColourPresets_IndigoViolet",
IsBrandPreset: false,
Colours: BuildIndigoViolet()),
};
}
// The Default preset spiegelt 1:1 die Werte aus ChatTypeExt.DefaultColor.
// Channels ohne Default-Wert (return null) werden ausgelassen — wer sie
// anwenden will, behält seine aktuelle Farbe.
private static IReadOnlyDictionary<ChatType, uint> BuildDefault()
{
var dict = new Dictionary<ChatType, uint>();
foreach (var (_, types) in ChatTypeExt.SortOrder)
{
foreach (var type in types)
{
var def = type.DefaultColor();
if (def.HasValue)
dict[type] = def.Value;
}
}
return dict;
}
private static IReadOnlyDictionary<ChatType, uint> BuildHighContrast()
{
return new Dictionary<ChatType, uint>
{
[ChatType.Say] = ColourUtil.ComponentsToRgba(255, 255, 255),
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 192, 0),
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 96, 0),
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(255, 128, 255),
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(255, 128, 255),
[ChatType.Party] = ColourUtil.ComponentsToRgba(128, 192, 255),
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 128, 64),
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(96, 192, 255),
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(192, 255, 64),
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 128, 128),
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 192, 128),
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 255, 128),
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(192, 255, 128),
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(128, 255, 192),
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(128, 192, 255),
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(192, 128, 255),
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(255, 128, 192),
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(255, 96, 96),
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(255, 160, 96),
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(255, 255, 96),
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(160, 255, 96),
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(96, 255, 160),
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(96, 160, 255),
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(160, 96, 255),
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(255, 96, 160),
};
}
private static IReadOnlyDictionary<ChatType, uint> BuildPastell()
{
return new Dictionary<ChatType, uint>
{
[ChatType.Say] = ColourUtil.ComponentsToRgba(232, 232, 232),
[ChatType.Yell] = ColourUtil.ComponentsToRgba(245, 216, 155),
[ChatType.Shout] = ColourUtil.ComponentsToRgba(245, 176, 155),
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(224, 176, 224),
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(224, 176, 224),
[ChatType.Party] = ColourUtil.ComponentsToRgba(176, 204, 224),
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(224, 192, 160),
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(168, 200, 224),
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(200, 224, 176),
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(224, 176, 176),
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(224, 200, 176),
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(224, 224, 176),
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(200, 224, 176),
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(176, 224, 200),
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(176, 200, 224),
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(200, 176, 224),
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(224, 176, 200),
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(224, 160, 160),
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(224, 192, 160),
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(224, 224, 160),
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(192, 224, 160),
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(160, 224, 192),
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(160, 192, 224),
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(192, 160, 224),
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(224, 160, 192),
};
}
private static IReadOnlyDictionary<ChatType, uint> BuildDarkModeTuned()
{
return new Dictionary<ChatType, uint>
{
[ChatType.Say] = ColourUtil.ComponentsToRgba(240, 240, 240),
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 208, 64),
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 128, 64),
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(255, 160, 255),
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(255, 160, 255),
[ChatType.Party] = ColourUtil.ComponentsToRgba(160, 208, 255),
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 160, 96),
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(128, 200, 255),
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(192, 255, 96),
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 160, 160),
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 192, 160),
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 255, 160),
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(192, 255, 160),
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(160, 255, 192),
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(160, 192, 255),
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(192, 160, 255),
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(255, 160, 192),
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(255, 128, 128),
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(255, 160, 128),
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(255, 255, 128),
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(160, 255, 128),
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(128, 255, 160),
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(128, 160, 255),
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(160, 128, 255),
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(255, 128, 160),
};
}
// Hellion brand preset — Arctic Cyan + Ember Orange palette aus
// /mnt/ssd-fast/Projekte/hellion-media/hellion-media-website/BRANDING.md
// (Schema-Stand 2026-04-16). Channels sind über das ganze Brand-Spektrum
// verteilt damit jede Zeile auf einen Glance unterscheidbar ist:
// Cyan-Familie für Standard/Tell, Ember + Warning für laute Channels,
// Status-Farben (Success, Danger) für Linkshells. CrossLinkshells
// nutzen die dunkleren/sattersten Varianten derselben Hue-Familien.
private static IReadOnlyDictionary<ChatType, uint> BuildHellion()
{
return new Dictionary<ChatType, uint>
{
// Standard / Tell — Cyan-Familie (Brand-Primary)
[ChatType.Say] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light #4DD9E8
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan #00BED2
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark #0097A7
// Laute Channels — Ember/Warning
[ChatType.Yell] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning #F0AD4E
[ChatType.Shout] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember #F97316
// Gruppen-Channels — Success/Ember-dark/Cyan
[ChatType.Party] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success #5CB85C
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark #E85D04
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(77, 217, 232),// Cyan-light
// Linkshells 1-8 — über das ganze Brand-Spektrum verteilt
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(251, 146, 60), // Ember-light #FB923C
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(240, 173, 78), // Warning
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(92, 184, 92), // Success
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(77, 217, 232), // Cyan-light
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(249, 115, 22), // Brand Ember
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(217, 83, 79), // Danger #D9534F
// CrossWorld-Linkshells 1-8 — dunklere/sattersere Varianten
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(232, 93, 4), // Ember-dark
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(200, 140, 50), // Warning-dark
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(60, 140, 60), // Success-dark
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(0, 190, 210), // Brand Cyan
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(0, 151, 167), // Cyan-dark
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(0, 110, 130), // Cyan-darker
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(220, 90, 30), // Ember-medium
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(170, 60, 60), // Danger-dark
};
}
// Bonus preset — Night Blue, KAZAMA-Stimmungs-Theme aus
// /mnt/HDD-Data1/Obsidian/Vault/Systeme/KAZAMA/Theming/Night Blue + Indigo Violet Themes.md
// Klassisch, kühl, technisch — Marineblau-Tiefe ohne Lila-Anteil.
// Bewusst NICHT als Brand-Preset markiert (Vault-Boundary): die KAZAMA-Themes
// sind persönliche Stimmungs-Themes, nicht Teil des Hellion-Brand-Systems.
private static IReadOnlyDictionary<ChatType, uint> BuildNightBlue()
{
return new Dictionary<ChatType, uint>
{
// Standard / Tell — Royal Blue Akzent-Familie
[ChatType.Say] = ColourUtil.ComponentsToRgba(230, 237, 247), // text-primary
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(106, 176, 255),// akzent-hot
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
// Laute Channels — Warning/Danger Status-Töne
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74), // warning
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122), // danger
// Gruppen — Success/Akzent-Variations
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151), // success
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100), // warm-orange-light
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(74, 144, 226), // akzent-primary
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(140, 160, 191),// text-dim
// Linkshells 1-8 — über Spektrum verteilt
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(130, 220, 100),
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(61, 220, 151),
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(100, 200, 220),
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(106, 176, 255),
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(140, 160, 191),
// CrossWorld-Linkshells — gedämpfte Variants
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(90, 180, 80),
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(30, 170, 110),
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(50, 130, 170),
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(50, 110, 180),
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(90, 100, 130),
};
}
// Bonus preset — Indigo Violet, KAZAMA-Stimmungs-Theme aus demselben
// Vault-Doc. Warm-mystisch, "Galaxy/Glitter/Nordlicht" — tiefes Indigo
// mit kräftigem Violet-Akzent. Persönlicher Favorit (siehe Vault).
// Auch nicht als Brand-Preset (siehe NightBlue-Note oben).
private static IReadOnlyDictionary<ChatType, uint> BuildIndigoViolet()
{
return new Dictionary<ChatType, uint>
{
// Standard / Tell — Royal Violet Akzent-Familie
[ChatType.Say] = ColourUtil.ComponentsToRgba(240, 230, 255), // text-primary (light lavender)
[ChatType.TellIncoming] = ColourUtil.ComponentsToRgba(176, 124, 255),// akzent-hot
[ChatType.TellOutgoing] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
// Laute Channels — geteilt mit Night Blue (Status-Farben)
[ChatType.Yell] = ColourUtil.ComponentsToRgba(255, 184, 74),
[ChatType.Shout] = ColourUtil.ComponentsToRgba(255, 92, 122),
// Gruppen
[ChatType.Party] = ColourUtil.ComponentsToRgba(61, 220, 151),
[ChatType.Alliance] = ColourUtil.ComponentsToRgba(255, 144, 100),
[ChatType.FreeCompany] = ColourUtil.ComponentsToRgba(139, 77, 222), // akzent-primary
[ChatType.NoviceNetwork] = ColourUtil.ComponentsToRgba(168, 144, 208),// text-dim
// Linkshells 1-8
[ChatType.Linkshell1] = ColourUtil.ComponentsToRgba(255, 184, 74),
[ChatType.Linkshell2] = ColourUtil.ComponentsToRgba(255, 144, 100),
[ChatType.Linkshell3] = ColourUtil.ComponentsToRgba(255, 220, 130),
[ChatType.Linkshell4] = ColourUtil.ComponentsToRgba(200, 124, 255),
[ChatType.Linkshell5] = ColourUtil.ComponentsToRgba(176, 124, 255),
[ChatType.Linkshell6] = ColourUtil.ComponentsToRgba(139, 77, 222),
[ChatType.Linkshell7] = ColourUtil.ComponentsToRgba(130, 90, 200),
[ChatType.Linkshell8] = ColourUtil.ComponentsToRgba(168, 144, 208),
// CrossWorld-Linkshells
[ChatType.CrossLinkshell1] = ColourUtil.ComponentsToRgba(200, 130, 50),
[ChatType.CrossLinkshell2] = ColourUtil.ComponentsToRgba(220, 110, 80),
[ChatType.CrossLinkshell3] = ColourUtil.ComponentsToRgba(200, 180, 60),
[ChatType.CrossLinkshell4] = ColourUtil.ComponentsToRgba(130, 80, 180),
[ChatType.CrossLinkshell5] = ColourUtil.ComponentsToRgba(100, 60, 160),
[ChatType.CrossLinkshell6] = ColourUtil.ComponentsToRgba(91, 42, 154),
[ChatType.CrossLinkshell7] = ColourUtil.ComponentsToRgba(80, 50, 130),
[ChatType.CrossLinkshell8] = ColourUtil.ComponentsToRgba(117, 96, 160),
};
}
}
+359
View File
@@ -0,0 +1,359 @@
//------------------------------------------------------------------------------
// <auto-generated>
// Hand-maintained strongly-typed accessor for HellionStrings.resx.
// Mirrors the layout of Language.Designer.cs so the same Plugin.cs
// LanguageChanged handler can update Culture for both classes.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable
namespace HellionChat.Resources;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
internal class HellionStrings
{
private static global::System.Resources.ResourceManager? resourceMan;
private static global::System.Globalization.CultureInfo? resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal HellionStrings() { }
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager
{
get
{
if (resourceMan is null)
resourceMan = new global::System.Resources.ResourceManager("HellionChat.Resources.HellionStrings", typeof(HellionStrings).Assembly);
return resourceMan;
}
}
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo? Culture
{
get => resourceCulture;
set => resourceCulture = value;
}
private static string Get(string key)
=> ResourceManager.GetString(key, resourceCulture) ?? key;
internal static string Privacy_Tab_Title => Get(nameof(Privacy_Tab_Title));
internal static string Privacy_FilterEnabled_Name => Get(nameof(Privacy_FilterEnabled_Name));
internal static string Privacy_FilterEnabled_Description => Get(nameof(Privacy_FilterEnabled_Description));
internal static string Privacy_FilterEnabled_StorageOnly_Help => Get(nameof(Privacy_FilterEnabled_StorageOnly_Help));
internal static string Privacy_Filter_Tree_Heading => Get(nameof(Privacy_Filter_Tree_Heading));
internal static string Privacy_Whitelist_Help => Get(nameof(Privacy_Whitelist_Help));
internal static string Privacy_Preset_PrivacyFirst => Get(nameof(Privacy_Preset_PrivacyFirst));
internal static string Privacy_Preset_ClearAll => Get(nameof(Privacy_Preset_ClearAll));
internal static string Privacy_Preset_SelectAll => Get(nameof(Privacy_Preset_SelectAll));
internal static string Privacy_Group_DirectMessages => Get(nameof(Privacy_Group_DirectMessages));
internal static string Privacy_Group_PartyAlliance => Get(nameof(Privacy_Group_PartyAlliance));
internal static string Privacy_Group_FreeCompany => Get(nameof(Privacy_Group_FreeCompany));
internal static string Privacy_Group_Linkshells => Get(nameof(Privacy_Group_Linkshells));
internal static string Privacy_Group_CrossLinkshells => Get(nameof(Privacy_Group_CrossLinkshells));
internal static string Privacy_Group_ExtraChat => Get(nameof(Privacy_Group_ExtraChat));
internal static string Privacy_Group_PublicChat => Get(nameof(Privacy_Group_PublicChat));
internal static string Privacy_Group_SystemLogs => Get(nameof(Privacy_Group_SystemLogs));
internal static string Privacy_PersistUnknown_Name => Get(nameof(Privacy_PersistUnknown_Name));
internal static string Privacy_PersistUnknown_Description => Get(nameof(Privacy_PersistUnknown_Description));
internal static string Cleanup_Heading => Get(nameof(Cleanup_Heading));
internal static string Cleanup_Help_Intro => Get(nameof(Cleanup_Help_Intro));
internal static string Cleanup_Help_SavedNote => Get(nameof(Cleanup_Help_SavedNote));
internal static string Cleanup_Preview_Stale => Get(nameof(Cleanup_Preview_Stale));
internal static string Retention_Help_SavedNote => Get(nameof(Retention_Help_SavedNote));
internal static string Cleanup_RefreshPreview => Get(nameof(Cleanup_RefreshPreview));
internal static string Cleanup_NoPreview => Get(nameof(Cleanup_NoPreview));
internal static string Cleanup_TotalStored => Get(nameof(Cleanup_TotalStored));
internal static string Cleanup_WillKeep => Get(nameof(Cleanup_WillKeep));
internal static string Cleanup_WillDelete => Get(nameof(Cleanup_WillDelete));
internal static string Cleanup_Breakdown => Get(nameof(Cleanup_Breakdown));
internal static string Cleanup_Marker_Keep => Get(nameof(Cleanup_Marker_Keep));
internal static string Cleanup_Marker_Delete => Get(nameof(Cleanup_Marker_Delete));
internal static string Cleanup_Apply_Label => Get(nameof(Cleanup_Apply_Label));
internal static string Cleanup_Apply_Tooltip => Get(nameof(Cleanup_Apply_Tooltip));
internal static string Cleanup_Running => Get(nameof(Cleanup_Running));
internal static string Cleanup_PreviewError => Get(nameof(Cleanup_PreviewError));
internal static string Cleanup_Success => Get(nameof(Cleanup_Success));
internal static string Cleanup_Error => Get(nameof(Cleanup_Error));
internal static string Retention_Heading => Get(nameof(Retention_Heading));
internal static string Retention_Enabled_Name => Get(nameof(Retention_Enabled_Name));
internal static string Retention_Enabled_Description => Get(nameof(Retention_Enabled_Description));
internal static string Retention_Default_Label => Get(nameof(Retention_Default_Label));
internal static string Retention_Default_Help => Get(nameof(Retention_Default_Help));
internal static string Retention_Reset_Spec => Get(nameof(Retention_Reset_Spec));
internal static string Retention_Clear_Overrides => Get(nameof(Retention_Clear_Overrides));
internal static string Retention_Tree_Heading => Get(nameof(Retention_Tree_Heading));
internal static string Retention_Tag_Override => Get(nameof(Retention_Tag_Override));
internal static string Retention_Tag_Spec => Get(nameof(Retention_Tag_Spec));
internal static string Retention_Tag_Global => Get(nameof(Retention_Tag_Global));
internal static string Retention_Reset_Button => Get(nameof(Retention_Reset_Button));
internal static string Retention_Apply_Label => Get(nameof(Retention_Apply_Label));
internal static string Retention_Apply_Tooltip => Get(nameof(Retention_Apply_Tooltip));
internal static string Retention_Running => Get(nameof(Retention_Running));
internal static string Retention_LastRun_Never => Get(nameof(Retention_LastRun_Never));
internal static string Retention_LastRun_At => Get(nameof(Retention_LastRun_At));
internal static string Retention_Success => Get(nameof(Retention_Success));
internal static string Retention_Error => Get(nameof(Retention_Error));
internal static string Wizard_Title => Get(nameof(Wizard_Title));
internal static string Wizard_Intro => Get(nameof(Wizard_Intro));
internal static string Wizard_Profile_PrivacyFirst_Heading => Get(nameof(Wizard_Profile_PrivacyFirst_Heading));
internal static string Wizard_Profile_PrivacyFirst_Description => Get(nameof(Wizard_Profile_PrivacyFirst_Description));
internal static string Wizard_Profile_PrivacyFirst_Apply => Get(nameof(Wizard_Profile_PrivacyFirst_Apply));
internal static string Wizard_Profile_Casual_Heading => Get(nameof(Wizard_Profile_Casual_Heading));
internal static string Wizard_Profile_Casual_Description => Get(nameof(Wizard_Profile_Casual_Description));
internal static string Wizard_Profile_Casual_Apply => Get(nameof(Wizard_Profile_Casual_Apply));
internal static string Wizard_Profile_FullHistory_Heading => Get(nameof(Wizard_Profile_FullHistory_Heading));
internal static string Wizard_Profile_FullHistory_Description => Get(nameof(Wizard_Profile_FullHistory_Description));
internal static string Wizard_Profile_FullHistory_GdprWarning => Get(nameof(Wizard_Profile_FullHistory_GdprWarning));
internal static string Wizard_Profile_FullHistory_Apply => Get(nameof(Wizard_Profile_FullHistory_Apply));
internal static string Wizard_Reopen_Button => Get(nameof(Wizard_Reopen_Button));
internal static string Export_Heading => Get(nameof(Export_Heading));
internal static string Export_Help => Get(nameof(Export_Help));
internal static string Export_Range_Label => Get(nameof(Export_Range_Label));
internal static string Export_Sender_Label => Get(nameof(Export_Sender_Label));
internal static string Export_Channels_Heading => Get(nameof(Export_Channels_Heading));
internal static string Export_Channels_AllOff => Get(nameof(Export_Channels_AllOff));
internal static string Export_Format_Label => Get(nameof(Export_Format_Label));
internal static string Export_Format_Markdown => Get(nameof(Export_Format_Markdown));
internal static string Export_Format_Json => Get(nameof(Export_Format_Json));
internal static string Export_Format_Csv => Get(nameof(Export_Format_Csv));
internal static string Export_Button => Get(nameof(Export_Button));
internal static string Export_Dialog_Title => Get(nameof(Export_Dialog_Title));
internal static string Export_Running => Get(nameof(Export_Running));
internal static string Export_Success => Get(nameof(Export_Success));
internal static string Export_Empty => Get(nameof(Export_Empty));
internal static string Export_Error => Get(nameof(Export_Error));
internal static string Theme_Enabled_Name => Get(nameof(Theme_Enabled_Name));
internal static string Theme_Enabled_Description => Get(nameof(Theme_Enabled_Description));
internal static string Theme_WindowOpacity_Label => Get(nameof(Theme_WindowOpacity_Label));
internal static string Theme_WindowOpacity_Help => Get(nameof(Theme_WindowOpacity_Help));
internal static string Theme_UseHellionFont_Name => Get(nameof(Theme_UseHellionFont_Name));
internal static string Theme_UseHellionFont_Description => Get(nameof(Theme_UseHellionFont_Description));
internal static string About_Maintainer_Heading => Get(nameof(About_Maintainer_Heading));
internal static string About_Maintainer_Body => Get(nameof(About_Maintainer_Body));
internal static string About_Maintainer_Website_Label => Get(nameof(About_Maintainer_Website_Label));
internal static string About_Mission_Heading => Get(nameof(About_Mission_Heading));
internal static string About_Mission_P1 => Get(nameof(About_Mission_P1));
internal static string About_Mission_P2 => Get(nameof(About_Mission_P2));
internal static string About_Mission_P3 => Get(nameof(About_Mission_P3));
internal static string About_BuiltOn_Heading => Get(nameof(About_BuiltOn_Heading));
internal static string About_BuiltOn_P1 => Get(nameof(About_BuiltOn_P1));
internal static string About_BuiltOn_P2 => Get(nameof(About_BuiltOn_P2));
internal static string About_BuiltOn_Upstream_Label => Get(nameof(About_BuiltOn_Upstream_Label));
internal static string About_License_Heading => Get(nameof(About_License_Heading));
internal static string About_License_P1 => Get(nameof(About_License_P1));
internal static string About_License_P2 => Get(nameof(About_License_P2));
internal static string About_License_P3 => Get(nameof(About_License_P3));
internal static string About_SE_Heading => Get(nameof(About_SE_Heading));
internal static string About_SE_P1 => Get(nameof(About_SE_P1));
internal static string About_SE_P2 => Get(nameof(About_SE_P2));
internal static string About_Localization_Heading => Get(nameof(About_Localization_Heading));
internal static string About_Localization_P1 => Get(nameof(About_Localization_P1));
internal static string About_Localization_P2 => Get(nameof(About_Localization_P2));
internal static string About_Translators_TreeNode => Get(nameof(About_Translators_TreeNode));
// Hellion Chat — Auto-Tell-Tabs runtime strings
internal static string AutoTellTabs_SectionHeader => Get(nameof(AutoTellTabs_SectionHeader));
internal static string AutoTellTabs_HistorySeparator => Get(nameof(AutoTellTabs_HistorySeparator));
internal static string AutoTellTabs_HistoryLoadError => Get(nameof(AutoTellTabs_HistoryLoadError));
internal static string AutoTellTabs_GreetedTooltip => Get(nameof(AutoTellTabs_GreetedTooltip));
internal static string AutoTellTabs_UnGreetedTooltip => Get(nameof(AutoTellTabs_UnGreetedTooltip));
// Hellion Chat — Auto-Tell-Tabs Chat settings tab
internal static string ChatLog_AutoTellTabs_Section_Title => Get(nameof(ChatLog_AutoTellTabs_Section_Title));
internal static string ChatLog_AutoTellTabs_Enable_Name => Get(nameof(ChatLog_AutoTellTabs_Enable_Name));
internal static string ChatLog_AutoTellTabs_Enable_Description => Get(nameof(ChatLog_AutoTellTabs_Enable_Description));
internal static string ChatLog_AutoTellTabs_Limit_Name => Get(nameof(ChatLog_AutoTellTabs_Limit_Name));
internal static string ChatLog_AutoTellTabs_Limit_Description => Get(nameof(ChatLog_AutoTellTabs_Limit_Description));
internal static string ChatLog_AutoTellTabs_Compact_Name => Get(nameof(ChatLog_AutoTellTabs_Compact_Name));
internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description));
internal static string ChatLog_AutoTellTabs_GreetedToggle_Name => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Name));
internal static string ChatLog_AutoTellTabs_GreetedToggle_Description => Get(nameof(ChatLog_AutoTellTabs_GreetedToggle_Description));
internal static string ChatLog_AutoTellTabs_OpenAsPopout_Name => Get(nameof(ChatLog_AutoTellTabs_OpenAsPopout_Name));
internal static string ChatLog_AutoTellTabs_OpenAsPopout_Description => Get(nameof(ChatLog_AutoTellTabs_OpenAsPopout_Description));
internal static string ChatLog_AutoTellTabs_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint));
internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint));
// Hellion Chat — Auto-Tell-Tabs Privacy settings tab
internal static string Privacy_AutoTellTabs_Section_Title => Get(nameof(Privacy_AutoTellTabs_Section_Title));
internal static string Privacy_AutoTellTabs_Preload_Name => Get(nameof(Privacy_AutoTellTabs_Preload_Name));
internal static string Privacy_AutoTellTabs_Preload_Description => Get(nameof(Privacy_AutoTellTabs_Preload_Description));
internal static string Privacy_AutoTellTabs_Preload_Hint => Get(nameof(Privacy_AutoTellTabs_Preload_Hint));
// Hellion Chat — Settings UX Polish v10 wipe migration
internal static string SettingsRefactor_Migration_Title => Get(nameof(SettingsRefactor_Migration_Title));
internal static string SettingsRefactor_Migration_Content => Get(nameof(SettingsRefactor_Migration_Content));
// Hellion Chat — Settings UX Polish 8-tab structure
internal static string Settings_Tab_General => Get(nameof(Settings_Tab_General));
internal static string Settings_Tab_Appearance => Get(nameof(Settings_Tab_Appearance));
internal static string Settings_Tab_Window => Get(nameof(Settings_Tab_Window));
internal static string Settings_Tab_Chat => Get(nameof(Settings_Tab_Chat));
internal static string Settings_Tab_Tabs => Get(nameof(Settings_Tab_Tabs));
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));
internal static string Settings_General_Performance_Heading => Get(nameof(Settings_General_Performance_Heading));
internal static string Settings_General_Language_Heading => Get(nameof(Settings_General_Language_Heading));
// Hellion Chat — Appearance-Tab section headings
internal static string Settings_Appearance_Theme_Heading => Get(nameof(Settings_Appearance_Theme_Heading));
internal static string Settings_Appearance_Fonts_Heading => Get(nameof(Settings_Appearance_Fonts_Heading));
internal static string Settings_Appearance_Colours_Heading => Get(nameof(Settings_Appearance_Colours_Heading));
internal static string Settings_Appearance_Timestamps_Heading => Get(nameof(Settings_Appearance_Timestamps_Heading));
// Hellion Chat — Window-Tab section headings
internal static string Settings_Window_Hide_Heading => Get(nameof(Settings_Window_Hide_Heading));
internal static string Settings_Window_InactivityHide_Heading => Get(nameof(Settings_Window_InactivityHide_Heading));
internal static string Settings_Window_Frame_Heading => Get(nameof(Settings_Window_Frame_Heading));
internal static string Settings_Window_Tooltips_Heading => Get(nameof(Settings_Window_Tooltips_Heading));
// Hellion Chat — Chat-Tab section headings
internal static string Settings_Chat_AutoTellTabs_Heading => Get(nameof(Settings_Chat_AutoTellTabs_Heading));
internal static string Settings_Chat_Behaviour_Heading => Get(nameof(Settings_Chat_Behaviour_Heading));
internal static string Settings_Chat_Preview_Heading => Get(nameof(Settings_Chat_Preview_Heading));
internal static string Settings_Chat_Emotes_Heading => Get(nameof(Settings_Chat_Emotes_Heading));
// Hellion Chat — Database-Tab section headings
internal static string Settings_Database_Storage_Heading => Get(nameof(Settings_Database_Storage_Heading));
internal static string Settings_Database_Viewer_Heading => Get(nameof(Settings_Database_Viewer_Heading));
internal static string Settings_Database_Stats_Heading => Get(nameof(Settings_Database_Stats_Heading));
// Hellion Chat — Information-Tab section headings
internal static string Settings_Information_VersionInfo_Heading => Get(nameof(Settings_Information_VersionInfo_Heading));
internal static string Settings_Information_About_Heading => Get(nameof(Settings_Information_About_Heading));
internal static string Settings_Information_Changelog_Heading => Get(nameof(Settings_Information_Changelog_Heading));
// Hellion Chat — Default tab presets (channel-themed)
internal static string Tabs_Presets_System => Get(nameof(Tabs_Presets_System));
internal static string Tabs_Presets_FreeCompany => Get(nameof(Tabs_Presets_FreeCompany));
internal static string Tabs_Presets_Party => Get(nameof(Tabs_Presets_Party));
internal static string Tabs_Presets_Beginner => Get(nameof(Tabs_Presets_Beginner));
internal static string Tabs_Presets_Linkshell => Get(nameof(Tabs_Presets_Linkshell));
internal static string Tabs_Presets_Linkshell_Hint => Get(nameof(Tabs_Presets_Linkshell_Hint));
// Hellion Chat — v1.2.0 per-tab icon override
internal static string Tabs_Icon_Label => Get(nameof(Tabs_Icon_Label));
internal static string Tabs_Icon_HelpMarker => Get(nameof(Tabs_Icon_HelpMarker));
internal static string Tabs_Icon_DefaultOption => Get(nameof(Tabs_Icon_DefaultOption));
// Hellion Chat — v0.6.0 chat colour presets (display labels)
internal static string ChatColourPresets_Default => Get(nameof(ChatColourPresets_Default));
internal static string ChatColourPresets_HighContrast => Get(nameof(ChatColourPresets_HighContrast));
internal static string ChatColourPresets_Pastell => Get(nameof(ChatColourPresets_Pastell));
internal static string ChatColourPresets_DarkModeTuned => Get(nameof(ChatColourPresets_DarkModeTuned));
internal static string ChatColourPresets_Hellion => Get(nameof(ChatColourPresets_Hellion));
internal static string ChatColourPresets_NightBlue => Get(nameof(ChatColourPresets_NightBlue));
internal static string ChatColourPresets_IndigoViolet => Get(nameof(ChatColourPresets_IndigoViolet));
// Hellion Chat — v0.6.0 chat colour presets section copy
internal static string Settings_Appearance_Colours_PresetsHint => Get(nameof(Settings_Appearance_Colours_PresetsHint));
// Hellion Chat — v0.6.0 pop-out input master switch
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));
internal static string Popout_v060_HintOpenSettings => Get(nameof(Popout_v060_HintOpenSettings));
// Hellion Chat — v0.6.1 pop-out header hint banner (discoverability)
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));
// Hellion Chat — v1.2.0 Bottom-Status-Bar Privacy-Badge labels
internal static string StatusBar_Privacy_Enabled => Get(nameof(StatusBar_Privacy_Enabled));
internal static string StatusBar_Privacy_Open => Get(nameof(StatusBar_Privacy_Open));
// Hellion Chat — v1.2.0 Appearance / Compact-Density toggle
internal static string Appearance_UseCompactDensity_Name => Get(nameof(Appearance_UseCompactDensity_Name));
internal static string Appearance_UseCompactDensity_Description => Get(nameof(Appearance_UseCompactDensity_Description));
// Hellion Chat — v1.2.1 Settings Cleanup: new card titles + subtexts
internal static string Settings_Card_ThemeAndLayout_Title => Get(nameof(Settings_Card_ThemeAndLayout_Title));
internal static string Settings_Card_ThemeAndLayout_Subtext => Get(nameof(Settings_Card_ThemeAndLayout_Subtext));
internal static string Settings_Card_FontsAndColours_Title => Get(nameof(Settings_Card_FontsAndColours_Title));
internal static string Settings_Card_FontsAndColours_Subtext => Get(nameof(Settings_Card_FontsAndColours_Subtext));
internal static string Settings_Card_DataManagement_Title => Get(nameof(Settings_Card_DataManagement_Title));
internal static string Settings_Card_DataManagement_Subtext => Get(nameof(Settings_Card_DataManagement_Subtext));
// Hellion Chat — v1.2.1 Theme & Layout tab section headings + WindowOpacity slider
internal static string Settings_ThemeAndLayout_Theme_Heading => Get(nameof(Settings_ThemeAndLayout_Theme_Heading));
internal static string Settings_ThemeAndLayout_WindowStyle_Heading => Get(nameof(Settings_ThemeAndLayout_WindowStyle_Heading));
internal static string Settings_ThemeAndLayout_TimestampStyle_Heading => Get(nameof(Settings_ThemeAndLayout_TimestampStyle_Heading));
internal static string Settings_ThemeAndLayout_WindowOpacity_Name => Get(nameof(Settings_ThemeAndLayout_WindowOpacity_Name));
internal static string Settings_ThemeAndLayout_WindowOpacity_Description => Get(nameof(Settings_ThemeAndLayout_WindowOpacity_Description));
// Hellion Chat — v1.2.1 Fonts & Colours tab section headings
internal static string Settings_FontsAndColours_Fonts_Heading => Get(nameof(Settings_FontsAndColours_Fonts_Heading));
internal static string Settings_FontsAndColours_Colours_Heading => Get(nameof(Settings_FontsAndColours_Colours_Heading));
// Hellion Chat — v1.2.1 Data Management tab section headings
internal static string Settings_DataManagement_Storage_Heading => Get(nameof(Settings_DataManagement_Storage_Heading));
internal static string Settings_DataManagement_Retention_Heading => Get(nameof(Settings_DataManagement_Retention_Heading));
internal static string Settings_DataManagement_Cleanup_Heading => Get(nameof(Settings_DataManagement_Cleanup_Heading));
internal static string Settings_DataManagement_Export_Heading => Get(nameof(Settings_DataManagement_Export_Heading));
internal static string Settings_DataManagement_DbViewer_Heading => Get(nameof(Settings_DataManagement_DbViewer_Heading));
internal static string Settings_DataManagement_Advanced_Heading => Get(nameof(Settings_DataManagement_Advanced_Heading));
// Hellion Chat — v1.2.1 Window-tab Behaviour heading (replaces Frame heading)
internal static string Settings_Window_Frame_Behaviour_Heading => Get(nameof(Settings_Window_Frame_Behaviour_Heading));
// Hellion Chat — v1.2.1 Migration v15 → v16 toast
internal static string Migration_v16_OverrideStyle_Toast => Get(nameof(Migration_v16_OverrideStyle_Toast));
}
@@ -19,11 +19,14 @@
<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>
</data>
<data name="Privacy_Filter_Tree_Heading" xml:space="preserve">
<value>Privacy-Filter und Whitelist</value>
</data>
<data name="Privacy_Whitelist_Help" xml:space="preserve">
<value>Wähle aus, welche Kanäle in die lokale Datenbank gespeichert werden. Standard nach Datensparsamkeit: nur deine eigenen Konversationen. Über die Buttons unten kannst du eine Voreinstellung anwenden.</value>
</data>
@@ -75,6 +78,12 @@
<data name="Cleanup_Help_SavedNote" xml:space="preserve">
<value>Das Aufräumen nutzt deine GESPEICHERTE Whitelist (Plugin.Config), nicht ungespeicherte Änderungen oben. Klicke zuerst Speichern, wenn du deine aktuellen Änderungen anwenden willst.</value>
</data>
<data name="Retention_Help_SavedNote" xml:space="preserve">
<value>Der manuelle Lauf nutzt deine GESPEICHERTE Retention-Policy, nicht die Slider-Werte oben. Klicke zuerst Speichern, wenn der Lauf deine aktuellen Änderungen anwenden soll.</value>
</data>
<data name="Cleanup_Preview_Stale" xml:space="preserve">
<value>Vorschau veraltet, deine Whitelist hat sich seit dem letzten Aktualisieren geändert. Klicke Aktualisieren, um neu zu berechnen.</value>
</data>
<data name="Cleanup_RefreshPreview" xml:space="preserve">
<value>Vorschau aktualisieren</value>
</data>
@@ -174,18 +183,6 @@
<data name="Retention_Error" xml:space="preserve">
<value>Aufbewahrungs-Bereinigung fehlgeschlagen, siehe /xllog</value>
</data>
<data name="Migration_Notification_Title" xml:space="preserve">
<value>Hellion Chat</value>
</data>
<data name="Migration_Notification_Content" xml:space="preserve">
<value>Datenschutz-Filter ist standardmäßig aktiviert. Einstellungen → Datenschutz zum Anpassen.</value>
</data>
<data name="Migration_Webinterface_Removed_Title" xml:space="preserve">
<value>Hellion Chat 0.2.0</value>
</data>
<data name="Migration_Webinterface_Removed_Content" xml:space="preserve">
<value>Das Webinterface wurde in dieser Version entfernt, weil es nicht auf das Datenschutz-Niveau gehärtet werden konnte das Hellion Chat standardmäßig zusichert. Falls du es genutzt hast, schau bitte in die README für Hintergründe.</value>
</data>
<data name="Wizard_Title" xml:space="preserve">
<value>Hellion Chat — Willkommen</value>
</data>
@@ -214,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>
@@ -273,9 +270,6 @@
<data name="Export_Error" xml:space="preserve">
<value>Export fehlgeschlagen, siehe /xllog</value>
</data>
<data name="Theme_Heading" xml:space="preserve">
<value>Erscheinungsbild</value>
</data>
<data name="Theme_Enabled_Name" xml:space="preserve">
<value>Hellion-Theme für alle Plugin-Fenster verwenden</value>
</data>
@@ -368,12 +362,6 @@
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (Runtime-Strings) -->
<data name="AutoTellTabs_Migration_Title" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="AutoTellTabs_Migration_Content" xml:space="preserve">
<value>Auto-Tell-Tabs sind ab Version 0.4.0 standardmäßig aktiv. Du kannst sie im Chat-Tab deaktivieren oder anpassen.</value>
</data>
<data name="AutoTellTabs_SectionHeader" xml:space="preserve">
<value>Aktive Tells</value>
</data>
@@ -418,6 +406,12 @@
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
<value>Fügt neben jedem Auto-Tell-Tab einen Klick-Button hinzu, um einen Gesprächspartner als bereits begrüßt zu markieren — der Tab-Name wird dann gedimmt. Nützlich für Club-Greeter, die parallel viele Konversationen führen. Standardmäßig aus.</value>
</data>
<data name="ChatLog_AutoTellTabs_OpenAsPopout_Name" xml:space="preserve">
<value>Neue /tell-Tabs direkt als Pop-Out öffnen</value>
</data>
<data name="ChatLog_AutoTellTabs_OpenAsPopout_Description" xml:space="preserve">
<value>Wenn aktiv, wird jeder neu angelegte /tell-Tab sofort als eigenes Fenster geöffnet. Beim Schließen des Fensters kehrt der Tab in die Seitenleiste zurück.</value>
</data>
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
<value>Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen.</value>
</data>
@@ -438,4 +432,363 @@
<data name="Privacy_AutoTellTabs_Preload_Hint" xml:space="preserve">
<value>Greift nur, wenn Auto-Tell-Tabs im Chat-Tab aktiviert sind.</value>
</data>
<!-- Hellion Chat — Settings UX Polish v10 Wipe-Migration -->
<data name="SettingsRefactor_Migration_Title" xml:space="preserve">
<value>Settings umstrukturiert</value>
</data>
<data name="SettingsRefactor_Migration_Content" xml:space="preserve">
<value>Hellion Chat 0.5.0 hat die Settings in thematische Tabs umstrukturiert. Deine Chat-Datenbank und dein Nachrichtenverlauf bleiben unverändert. Settings wurden auf Defaults zurückgesetzt. Falls du das Privacy-Profil neu wählen willst, findest du den Reopen-Button im Datenschutz-Tab. Ein Backup der vorherigen Config liegt unter HellionChat.json.pre-v10-backup neben der aktiven Config-Datei.</value>
</data>
<!-- Hellion Chat — Settings UX Polish 8-Tab-Struktur -->
<data name="Settings_Tab_General" xml:space="preserve">
<value>Allgemein</value>
</data>
<data name="Settings_Tab_Appearance" xml:space="preserve">
<value>Aussehen</value>
</data>
<data name="Settings_Tab_Window" xml:space="preserve">
<value>Fenster</value>
</data>
<data name="Settings_Tab_Chat" xml:space="preserve">
<value>Chat</value>
</data>
<data name="Settings_Tab_Tabs" xml:space="preserve">
<value>Kanäle</value>
</data>
<data name="Settings_Tab_Database" xml:space="preserve">
<value>Datenbank</value>
</data>
<data name="Settings_Tab_Information" xml:space="preserve">
<value>Über</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Allgemein-Tabs -->
<data name="Settings_General_Input_Heading" xml:space="preserve">
<value>Eingabe</value>
</data>
<data name="Settings_General_Audio_Heading" xml:space="preserve">
<value>Audio &amp; Benachrichtigungen</value>
</data>
<data name="Settings_General_Performance_Heading" xml:space="preserve">
<value>Performance</value>
</data>
<data name="Settings_General_Language_Heading" xml:space="preserve">
<value>Sprache &amp; Eingabe-Hilfen</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Aussehen-Tabs -->
<data name="Settings_Appearance_Theme_Heading" xml:space="preserve">
<value>Theme</value>
</data>
<data name="Settings_Appearance_Fonts_Heading" xml:space="preserve">
<value>Schriftarten</value>
</data>
<data name="Settings_Appearance_Colours_Heading" xml:space="preserve">
<value>Chat-Farben</value>
</data>
<data name="Settings_Appearance_Timestamps_Heading" xml:space="preserve">
<value>Zeitstempel</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Fenster-Tabs -->
<data name="Settings_Window_Hide_Heading" xml:space="preserve">
<value>Verstecken</value>
</data>
<data name="Settings_Window_InactivityHide_Heading" xml:space="preserve">
<value>Inaktivitäts-Verstecken</value>
</data>
<data name="Settings_Window_Frame_Heading" xml:space="preserve">
<value>Fenster-Rahmen</value>
</data>
<data name="Settings_Window_Tooltips_Heading" xml:space="preserve">
<value>Tooltips</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Chat-Tabs -->
<data name="Settings_Chat_AutoTellTabs_Heading" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="Settings_Chat_Behaviour_Heading" xml:space="preserve">
<value>Nachrichten-Verhalten</value>
</data>
<data name="Settings_Chat_Preview_Heading" xml:space="preserve">
<value>Vorschau</value>
</data>
<data name="Settings_Chat_Emotes_Heading" xml:space="preserve">
<value>Emotes</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Database-Tabs -->
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
<value>Speicherung</value>
</data>
<data name="Settings_Database_Viewer_Heading" xml:space="preserve">
<value>Übersicht</value>
</data>
<data name="Settings_Database_Stats_Heading" xml:space="preserve">
<value>Wartung</value>
</data>
<!-- Hellion Chat — Sektions-Überschriften des Information-Tabs -->
<data name="Settings_Information_VersionInfo_Heading" xml:space="preserve">
<value>Versionsinfo</value>
</data>
<data name="Settings_Information_About_Heading" xml:space="preserve">
<value>Über HellionChat</value>
</data>
<data name="Settings_Information_Changelog_Heading" xml:space="preserve">
<value>Changelog</value>
</data>
<!-- Hellion Chat — Default-Tab-Presets (kanalspezifisch) -->
<data name="Tabs_Presets_System" xml:space="preserve">
<value>System</value>
</data>
<data name="Tabs_Presets_FreeCompany" xml:space="preserve">
<value>Free Company</value>
</data>
<data name="Tabs_Presets_Party" xml:space="preserve">
<value>Gruppe</value>
</data>
<data name="Tabs_Presets_Beginner" xml:space="preserve">
<value>Neulinge</value>
</data>
<data name="Tabs_Presets_Linkshell" xml:space="preserve">
<value>Linkshell</value>
</data>
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
<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>
<!-- Hellion Chat — v1.2.0 per-tab icon override -->
<data name="Tabs_Icon_Label" xml:space="preserve">
<value>Tab-Icon</value>
</data>
<data name="Tabs_Icon_HelpMarker" xml:space="preserve">
<value>FontAwesome-Glyph für die Sidebar. Default greift auf Tab-Name oder Channel-Typ.</value>
</data>
<data name="Tabs_Icon_DefaultOption" xml:space="preserve">
<value>(Default-Mapping)</value>
</data>
<data name="ChatColourPresets_Default" xml:space="preserve">
<value>Klassik (Chat 2 Default)</value>
</data>
<data name="ChatColourPresets_HighContrast" xml:space="preserve">
<value>Hoher Kontrast</value>
</data>
<data name="ChatColourPresets_Pastell" xml:space="preserve">
<value>Pastell</value>
</data>
<data name="ChatColourPresets_DarkModeTuned" xml:space="preserve">
<value>Dunkelmodus-optimiert</value>
</data>
<data name="ChatColourPresets_Hellion" xml:space="preserve">
<value>Hellion</value>
</data>
<data name="ChatColourPresets_NightBlue" xml:space="preserve">
<value>Night Blue</value>
</data>
<data name="ChatColourPresets_IndigoViolet" xml:space="preserve">
<value>Indigo Violet</value>
</data>
<data name="Settings_Appearance_Colours_PresetsHint" xml:space="preserve">
<value>Tipp: Presets überschreiben deine aktuellen Channel-Farben sofort.</value>
</data>
<data name="Settings_Window_PopOutInputEnabled_Name" xml:space="preserve">
<value>Eingabe in Pop-Outs aktivieren</value>
</data>
<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>
<data name="Popout_v060_HintAck" xml:space="preserve">
<value>Verstanden</value>
</data>
<data name="Popout_v060_HintOpenSettings" xml:space="preserve">
<value>Fenster-Settings öffnen</value>
</data>
<data name="Hint_v061_PopOutHeader_Body" xml:space="preserve">
<value>Du kannst jeden Chat-Tab als eigenes Fenster öffnen. Klicke auf das Fenster-Symbol oben rechts oder rechtsklicke den Tab. Neu in v0.6.1: die Pop-Out-Eingabe ist standardmäßig aktiv (abschaltbar unter Einstellungen → Fenster).</value>
</data>
<data name="Hint_v061_PopOutHeader_Ack" xml:space="preserve">
<value>Verstanden</value>
</data>
<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>Plugin-globale Einstellungen — Sprache, Eingabe, Audio, Performance.</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>Verhalten des Fensters — wann es da ist, ob es bewegt werden kann.</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>Wie Nachrichten angezeigt werden — Tells, Vorschau, Verhalten, Emotes.</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-Verwaltung — eigene Chat-Tabs anlegen und konfigurieren.</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>Was darf gespeichert werden — Privacy-Filter pro Channel.</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 das Plugin — Version, Mission, Lizenz, Changelog.</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>
<data name="StatusBar_Privacy_Enabled" xml:space="preserve">
<value>Privacy-First</value>
</data>
<data name="StatusBar_Privacy_Open" xml:space="preserve">
<value>Offen</value>
</data>
<data name="Appearance_UseCompactDensity_Name" xml:space="preserve">
<value>Kompakte Dichte</value>
</data>
<data name="Appearance_UseCompactDensity_Description" xml:space="preserve">
<value>Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen.</value>
</data>
<data name="Settings_Card_ThemeAndLayout_Title" xml:space="preserve">
<value>Theme &amp; Layout</value>
</data>
<data name="Settings_Card_ThemeAndLayout_Subtext" xml:space="preserve">
<value>Wie das Fenster aussieht — Theme, Rahmen, Zeitstempel-Style.</value>
</data>
<data name="Settings_Card_FontsAndColours_Title" xml:space="preserve">
<value>Schriften &amp; Farben</value>
</data>
<data name="Settings_Card_FontsAndColours_Subtext" xml:space="preserve">
<value>Lesbarkeit — Schriftart, Schriftgröße, Chat-Farben pro Channel.</value>
</data>
<data name="Settings_Card_DataManagement_Title" xml:space="preserve">
<value>Daten-Verwaltung</value>
</data>
<data name="Settings_Card_DataManagement_Subtext" xml:space="preserve">
<value>Was passiert mit gespeicherten Daten — Aufbewahrung, Aufräumen, Export, DB-Stats.</value>
</data>
<data name="Settings_ThemeAndLayout_Theme_Heading" xml:space="preserve">
<value>Theme</value>
</data>
<data name="Settings_ThemeAndLayout_WindowStyle_Heading" xml:space="preserve">
<value>Fenster-Style</value>
</data>
<data name="Settings_ThemeAndLayout_TimestampStyle_Heading" xml:space="preserve">
<value>Zeitstempel-Style</value>
</data>
<data name="Settings_ThemeAndLayout_WindowOpacity_Name" xml:space="preserve">
<value>Fenster-Transparenz</value>
</data>
<data name="Settings_ThemeAndLayout_WindowOpacity_Description" xml:space="preserve">
<value>Wie durchsichtig der Fensterhintergrund ist. Niedrigere Werte lassen mehr vom Spiel durchscheinen. Tipp: Dalamud's Per-Window-Menü (Hamburger in der Titelleiste) bietet pro Fenster eigene Overrides für Deckkraft, Hintergrund-Blur, Durchklick und Anpinnen — die haben Vorrang über diesen Slider für das jeweilige Fenster.</value>
</data>
<data name="Settings_FontsAndColours_Fonts_Heading" xml:space="preserve">
<value>Schriftarten</value>
</data>
<data name="Settings_FontsAndColours_Colours_Heading" xml:space="preserve">
<value>Chat-Farben</value>
</data>
<data name="Settings_DataManagement_Storage_Heading" xml:space="preserve">
<value>Speicherung</value>
</data>
<data name="Settings_DataManagement_Retention_Heading" xml:space="preserve">
<value>Aufbewahrung</value>
</data>
<data name="Settings_DataManagement_Cleanup_Heading" xml:space="preserve">
<value>Cleanup</value>
</data>
<data name="Settings_DataManagement_Export_Heading" xml:space="preserve">
<value>Export</value>
</data>
<data name="Settings_DataManagement_DbViewer_Heading" xml:space="preserve">
<value>Datenbank-Viewer</value>
</data>
<data name="Settings_DataManagement_Advanced_Heading" xml:space="preserve">
<value>Erweitert (Shift+Klick zum Öffnen)</value>
</data>
<data name="Settings_Window_Frame_Behaviour_Heading" xml:space="preserve">
<value>Verhalten</value>
</data>
<data name="Migration_v16_OverrideStyle_Toast" xml:space="preserve">
<value>Hellion Chat 1.2.1 hat das Settings-Menü neu sortiert und die alte „Stilüberschreiben"-Option entfernt (überholt durch das Theme-System aus 1.1.0). Deine restlichen Einstellungen bleiben unverändert. Die Fenster-Transparenz ist nach „Theme &amp; Layout" migriert. Ein Backup der vorherigen Config liegt unter pluginConfigs/HellionChat.json.pre-v16-backup neben der aktiven HellionChat.json.</value>
</data>
</root>
@@ -19,11 +19,14 @@
<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>
</data>
<data name="Privacy_Filter_Tree_Heading" xml:space="preserve">
<value>Privacy filter and whitelist</value>
</data>
<data name="Privacy_Whitelist_Help" xml:space="preserve">
<value>Pick which channels are stored in the local database. Privacy-First default: only your own conversations. Use the buttons below to apply a preset.</value>
</data>
@@ -75,6 +78,12 @@
<data name="Cleanup_Help_SavedNote" xml:space="preserve">
<value>Cleanup uses your SAVED whitelist (Plugin.Config), not unsaved edits above. Click Save first if you want to apply your current edits.</value>
</data>
<data name="Retention_Help_SavedNote" xml:space="preserve">
<value>The manual sweep uses your SAVED retention policy, not the slider values above. Click Save first if you want the run to apply your current edits.</value>
</data>
<data name="Cleanup_Preview_Stale" xml:space="preserve">
<value>Preview is out of date — your whitelist has changed since the last refresh. Click Refresh to recalculate.</value>
</data>
<data name="Cleanup_RefreshPreview" xml:space="preserve">
<value>Refresh preview</value>
</data>
@@ -174,18 +183,6 @@
<data name="Retention_Error" xml:space="preserve">
<value>Retention sweep failed, see /xllog</value>
</data>
<data name="Migration_Notification_Title" xml:space="preserve">
<value>Hellion Chat</value>
</data>
<data name="Migration_Notification_Content" xml:space="preserve">
<value>Privacy filter activated by default. Settings → Privacy to adjust.</value>
</data>
<data name="Migration_Webinterface_Removed_Title" xml:space="preserve">
<value>Hellion Chat 0.2.0</value>
</data>
<data name="Migration_Webinterface_Removed_Content" xml:space="preserve">
<value>The webinterface has been removed in this version because it could not be hardened to the privacy guarantees Hellion Chat makes by default. If you used it, please consult the README for context.</value>
</data>
<data name="Wizard_Title" xml:space="preserve">
<value>Hellion Chat — Welcome</value>
</data>
@@ -214,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>
@@ -273,9 +270,6 @@
<data name="Export_Error" xml:space="preserve">
<value>Export failed, see /xllog</value>
</data>
<data name="Theme_Heading" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="Theme_Enabled_Name" xml:space="preserve">
<value>Use the Hellion theme across all plugin windows</value>
</data>
@@ -368,12 +362,6 @@
</data>
<!-- Hellion Chat — Auto-Tell-Tabs (runtime strings) -->
<data name="AutoTellTabs_Migration_Title" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="AutoTellTabs_Migration_Content" xml:space="preserve">
<value>Auto-Tell-Tabs are enabled by default starting with version 0.4.0. You can disable or fine-tune them in the Chat tab.</value>
</data>
<data name="AutoTellTabs_SectionHeader" xml:space="preserve">
<value>Active Tells</value>
</data>
@@ -418,6 +406,12 @@
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve">
<value>Adds a click-to-toggle button next to each auto tell tab to mark a partner as already greeted, dimming the tab name when set. Useful for club greeters tracking many parallel conversations; off by default.</value>
</data>
<data name="ChatLog_AutoTellTabs_OpenAsPopout_Name" xml:space="preserve">
<value>Open new /tell tabs directly as pop-out</value>
</data>
<data name="ChatLog_AutoTellTabs_OpenAsPopout_Description" xml:space="preserve">
<value>When enabled, each newly created /tell tab opens directly as its own window. Closing the window returns the tab to the sidebar.</value>
</data>
<data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
<value>The number of preloaded tells is configured in the Privacy tab.</value>
</data>
@@ -438,4 +432,363 @@
<data name="Privacy_AutoTellTabs_Preload_Hint" xml:space="preserve">
<value>Only takes effect when auto tell tabs are enabled in the Chat tab.</value>
</data>
<!-- Hellion Chat — Settings UX Polish v10 wipe migration -->
<data name="SettingsRefactor_Migration_Title" xml:space="preserve">
<value>Settings reorganised</value>
</data>
<data name="SettingsRefactor_Migration_Content" xml:space="preserve">
<value>Hellion Chat 0.5.0 reorganised the settings into themed tabs. Your chat database and your message history stay untouched. Settings have been reset to defaults; if you want to pick a privacy profile again, the reopen button is in the Privacy tab. A backup of your previous config is at HellionChat.json.pre-v10-backup next to the live config file.</value>
</data>
<!-- Hellion Chat — Settings UX Polish 8-tab structure -->
<data name="Settings_Tab_General" xml:space="preserve">
<value>General</value>
</data>
<data name="Settings_Tab_Appearance" xml:space="preserve">
<value>Appearance</value>
</data>
<data name="Settings_Tab_Window" xml:space="preserve">
<value>Window</value>
</data>
<data name="Settings_Tab_Chat" xml:space="preserve">
<value>Chat</value>
</data>
<data name="Settings_Tab_Tabs" xml:space="preserve">
<value>Tabs</value>
</data>
<data name="Settings_Tab_Database" xml:space="preserve">
<value>Database</value>
</data>
<data name="Settings_Tab_Information" xml:space="preserve">
<value>Information</value>
</data>
<!-- Hellion Chat — General-Tab section headings -->
<data name="Settings_General_Input_Heading" xml:space="preserve">
<value>Input</value>
</data>
<data name="Settings_General_Audio_Heading" xml:space="preserve">
<value>Audio &amp; Notifications</value>
</data>
<data name="Settings_General_Performance_Heading" xml:space="preserve">
<value>Performance</value>
</data>
<data name="Settings_General_Language_Heading" xml:space="preserve">
<value>Language &amp; Input Helpers</value>
</data>
<!-- Hellion Chat — Appearance-Tab section headings -->
<data name="Settings_Appearance_Theme_Heading" xml:space="preserve">
<value>Theme</value>
</data>
<data name="Settings_Appearance_Fonts_Heading" xml:space="preserve">
<value>Fonts</value>
</data>
<data name="Settings_Appearance_Colours_Heading" xml:space="preserve">
<value>Chat Colours</value>
</data>
<data name="Settings_Appearance_Timestamps_Heading" xml:space="preserve">
<value>Timestamps</value>
</data>
<!-- Hellion Chat — Window-Tab section headings -->
<data name="Settings_Window_Hide_Heading" xml:space="preserve">
<value>Hide</value>
</data>
<data name="Settings_Window_InactivityHide_Heading" xml:space="preserve">
<value>Inactivity Hide</value>
</data>
<data name="Settings_Window_Frame_Heading" xml:space="preserve">
<value>Window Frame</value>
</data>
<data name="Settings_Window_Tooltips_Heading" xml:space="preserve">
<value>Tooltips</value>
</data>
<!-- Hellion Chat — Chat-Tab section headings -->
<data name="Settings_Chat_AutoTellTabs_Heading" xml:space="preserve">
<value>Auto-Tell-Tabs</value>
</data>
<data name="Settings_Chat_Behaviour_Heading" xml:space="preserve">
<value>Message Behaviour</value>
</data>
<data name="Settings_Chat_Preview_Heading" xml:space="preserve">
<value>Preview</value>
</data>
<data name="Settings_Chat_Emotes_Heading" xml:space="preserve">
<value>Emotes</value>
</data>
<!-- Hellion Chat — Database-Tab section headings -->
<data name="Settings_Database_Storage_Heading" xml:space="preserve">
<value>Storage</value>
</data>
<data name="Settings_Database_Viewer_Heading" xml:space="preserve">
<value>Overview</value>
</data>
<data name="Settings_Database_Stats_Heading" xml:space="preserve">
<value>Maintenance</value>
</data>
<!-- Hellion Chat — Information-Tab section headings -->
<data name="Settings_Information_VersionInfo_Heading" xml:space="preserve">
<value>Version Info</value>
</data>
<data name="Settings_Information_About_Heading" xml:space="preserve">
<value>About HellionChat</value>
</data>
<data name="Settings_Information_Changelog_Heading" xml:space="preserve">
<value>Changelog</value>
</data>
<!-- Hellion Chat — Default tab presets (channel-themed) -->
<data name="Tabs_Presets_System" xml:space="preserve">
<value>System</value>
</data>
<data name="Tabs_Presets_FreeCompany" xml:space="preserve">
<value>Free Company</value>
</data>
<data name="Tabs_Presets_Party" xml:space="preserve">
<value>Party</value>
</data>
<data name="Tabs_Presets_Beginner" xml:space="preserve">
<value>Beginner</value>
</data>
<data name="Tabs_Presets_Linkshell" xml:space="preserve">
<value>Linkshell</value>
</data>
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve">
<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>
<!-- Hellion Chat — v1.2.0 per-tab icon override -->
<data name="Tabs_Icon_Label" xml:space="preserve">
<value>Tab-Icon</value>
</data>
<data name="Tabs_Icon_HelpMarker" xml:space="preserve">
<value>FontAwesome-Glyph für die Sidebar. Default greift auf Tab-Name oder Channel-Typ.</value>
</data>
<data name="Tabs_Icon_DefaultOption" xml:space="preserve">
<value>(Default-Mapping)</value>
</data>
<data name="ChatColourPresets_Default" xml:space="preserve">
<value>Klassik (Chat 2 Default)</value>
</data>
<data name="ChatColourPresets_HighContrast" xml:space="preserve">
<value>High-Contrast</value>
</data>
<data name="ChatColourPresets_Pastell" xml:space="preserve">
<value>Pastell</value>
</data>
<data name="ChatColourPresets_DarkModeTuned" xml:space="preserve">
<value>Dark-Mode-Tuned</value>
</data>
<data name="ChatColourPresets_Hellion" xml:space="preserve">
<value>Hellion</value>
</data>
<data name="ChatColourPresets_NightBlue" xml:space="preserve">
<value>Night Blue</value>
</data>
<data name="ChatColourPresets_IndigoViolet" xml:space="preserve">
<value>Indigo Violet</value>
</data>
<data name="Settings_Appearance_Colours_PresetsHint" xml:space="preserve">
<value>Tip: presets overwrite your current channel colours immediately.</value>
</data>
<data name="Settings_Window_PopOutInputEnabled_Name" xml:space="preserve">
<value>Enable input in pop-outs</value>
</data>
<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>
<data name="Popout_v060_HintAck" xml:space="preserve">
<value>Got it</value>
</data>
<data name="Popout_v060_HintOpenSettings" xml:space="preserve">
<value>Open window settings</value>
</data>
<data name="Hint_v061_PopOutHeader_Body" xml:space="preserve">
<value>You can open any chat tab as its own window. Click the window icon in the top right or right-click the tab. New in v0.6.1: pop-out input is enabled by default (can be turned off under Settings → Window).</value>
</data>
<data name="Hint_v061_PopOutHeader_Ack" xml:space="preserve">
<value>Got it</value>
</data>
<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>Plugin-wide settings — language, input, audio, performance.</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 behaviour — when it shows, whether it can move.</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>How messages are displayed — tells, preview, behaviour, emotes.</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 management — create and configure your own chat 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>What's allowed to be stored — privacy filter per channel.</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 the plugin — version, mission, license, changelog.</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>
<data name="StatusBar_Privacy_Enabled" xml:space="preserve">
<value>Privacy-First</value>
</data>
<data name="StatusBar_Privacy_Open" xml:space="preserve">
<value>Open</value>
</data>
<data name="Appearance_UseCompactDensity_Name" xml:space="preserve">
<value>Compact Density</value>
</data>
<data name="Appearance_UseCompactDensity_Description" xml:space="preserve">
<value>Schaltet das Message-Layout vom Card-Row-Default zurück auf einzeilige `[HH:mm] Sender: Text` Zeilen.</value>
</data>
<data name="Settings_Card_ThemeAndLayout_Title" xml:space="preserve">
<value>Theme &amp; Layout</value>
</data>
<data name="Settings_Card_ThemeAndLayout_Subtext" xml:space="preserve">
<value>How the window looks — theme, frame, timestamp style.</value>
</data>
<data name="Settings_Card_FontsAndColours_Title" xml:space="preserve">
<value>Fonts &amp; Colours</value>
</data>
<data name="Settings_Card_FontsAndColours_Subtext" xml:space="preserve">
<value>Readability — font, font size, per-channel chat colours.</value>
</data>
<data name="Settings_Card_DataManagement_Title" xml:space="preserve">
<value>Data Management</value>
</data>
<data name="Settings_Card_DataManagement_Subtext" xml:space="preserve">
<value>What happens to stored data — retention, cleanup, export, DB stats.</value>
</data>
<data name="Settings_ThemeAndLayout_Theme_Heading" xml:space="preserve">
<value>Theme</value>
</data>
<data name="Settings_ThemeAndLayout_WindowStyle_Heading" xml:space="preserve">
<value>Window Style</value>
</data>
<data name="Settings_ThemeAndLayout_TimestampStyle_Heading" xml:space="preserve">
<value>Timestamp Style</value>
</data>
<data name="Settings_ThemeAndLayout_WindowOpacity_Name" xml:space="preserve">
<value>Window Transparency</value>
</data>
<data name="Settings_ThemeAndLayout_WindowOpacity_Description" xml:space="preserve">
<value>How transparent the window background is. Lower values let the game show through more. Tip: Dalamud's per-window menu (Hamburger in the title bar) gives you per-window overrides for opacity, background blur, click-through and pinning — those override this slider for that window.</value>
</data>
<data name="Settings_FontsAndColours_Fonts_Heading" xml:space="preserve">
<value>Fonts</value>
</data>
<data name="Settings_FontsAndColours_Colours_Heading" xml:space="preserve">
<value>Chat Colours</value>
</data>
<data name="Settings_DataManagement_Storage_Heading" xml:space="preserve">
<value>Storage</value>
</data>
<data name="Settings_DataManagement_Retention_Heading" xml:space="preserve">
<value>Retention</value>
</data>
<data name="Settings_DataManagement_Cleanup_Heading" xml:space="preserve">
<value>Cleanup</value>
</data>
<data name="Settings_DataManagement_Export_Heading" xml:space="preserve">
<value>Export</value>
</data>
<data name="Settings_DataManagement_DbViewer_Heading" xml:space="preserve">
<value>Database Viewer</value>
</data>
<data name="Settings_DataManagement_Advanced_Heading" xml:space="preserve">
<value>Advanced (Shift+Click to open)</value>
</data>
<data name="Settings_Window_Frame_Behaviour_Heading" xml:space="preserve">
<value>Behaviour</value>
</data>
<data name="Migration_v16_OverrideStyle_Toast" xml:space="preserve">
<value>Hellion Chat 1.2.1 reorganised the Settings menu and removed the legacy "Style override" option (made obsolete by the Themes system in 1.1.0). Your other settings are unchanged. Window opacity was migrated to Theme &amp; Layout. A backup of your previous config is at pluginConfigs/HellionChat.json.pre-v16-backup next to the live HellionChat.json.</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>
@@ -2660,7 +2678,25 @@ namespace ChatTwo.Resources {
return ResourceManager.GetString("Options_HideInBattle_Name", resourceCulture);
}
}
/// <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>

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