Compare commits

...

80 Commits

Author SHA1 Message Date
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
137 changed files with 3926 additions and 727 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
# Generated files # 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.
+2 -2
View File
@@ -16,7 +16,7 @@ body:
attributes: attributes:
label: HellionChat version label: HellionChat version
description: From Settings → Information → Version description: From Settings → Information → Version
placeholder: "0.5.1" placeholder: "0.5.4"
validations: validations:
required: true required: true
@@ -59,7 +59,7 @@ body:
id: log id: log
attributes: attributes:
label: Relevant /xllog excerpt label: Relevant /xllog excerpt
description: Filter for "HellionChat" or "ChatTwo" if the log is huge description: Filter for "HellionChat" if the log is huge
render: text render: text
- type: checkboxes - type: checkboxes
+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).
+1 -1
View File
@@ -5,7 +5,7 @@ updates:
# noise down while still catching transitive security advisories within # noise down while still catching transitive security advisories within
# a few days of disclosure. # a few days of disclosure.
- package-ecosystem: nuget - package-ecosystem: nuget
directory: /ChatTwo directory: /HellionChat
schedule: schedule:
interval: weekly interval: weekly
day: monday day: monday
+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.
+4 -4
View File
@@ -29,7 +29,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Setup .NET 10 - name: Setup .NET 10
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: 10.0.x dotnet-version: 10.0.x
@@ -42,15 +42,15 @@ jobs:
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
- name: Restore - name: Restore
run: dotnet restore ChatTwo/ChatTwo.csproj run: dotnet restore HellionChat/HellionChat.csproj
- name: Build (Release) - name: Build (Release)
run: dotnet build ChatTwo/ChatTwo.csproj --configuration Release --no-restore run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
- name: Upload build output - name: Upload build output
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: HellionChat-build-${{ github.run_number }} name: HellionChat-build-${{ github.run_number }}
path: ChatTwo/bin/Release/**/HellionChat/** path: HellionChat/bin/Release/**/HellionChat/**
if-no-files-found: warn if-no-files-found: warn
retention-days: 14 retention-days: 14
+7 -7
View File
@@ -42,7 +42,7 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Setup .NET 10 - name: Setup .NET 10
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: 10.0.x dotnet-version: 10.0.x
@@ -55,20 +55,20 @@ jobs:
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: csharp languages: csharp
build-mode: manual build-mode: manual
queries: security-extended queries: security-extended
- name: Restore - name: Restore
run: dotnet restore ChatTwo/ChatTwo.csproj run: dotnet restore HellionChat/HellionChat.csproj
- name: Build (Release) - name: Build (Release)
run: dotnet build ChatTwo/ChatTwo.csproj --configuration Release --no-restore run: dotnet build HellionChat/HellionChat.csproj --configuration Release --no-restore
- name: Perform CodeQL analysis - name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
with: with:
category: /language:csharp category: /language:csharp
@@ -82,12 +82,12 @@ jobs:
uses: actions/checkout@v6 uses: actions/checkout@v6
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
with: with:
languages: actions languages: actions
build-mode: none build-mode: none
- name: Perform CodeQL analysis - name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
with: with:
category: /language:actions category: /language:actions
+110 -7
View File
@@ -3,14 +3,29 @@ name: Release
# Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the # Triggered when a vX.Y.Z tag is pushed. Builds the plugin against the
# current Dalamud staging branch, locates the latest.zip produced by # current Dalamud staging branch, locates the latest.zip produced by
# DalamudPackager and attaches it to the matching GitHub Release. # DalamudPackager and attaches it to the matching GitHub Release.
# Does not consume any user-controlled event payload, only the tag name #
# (validated by the on.tags filter) and the steps output of the locate # User-controlled inputs touched by this workflow:
# step (path string from Get-ChildItem on a controlled directory). # - 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: on:
push: push:
tags: tags:
- 'v*' - '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: permissions:
contents: write contents: write
@@ -22,11 +37,17 @@ jobs:
timeout-minutes: 20 timeout-minutes: 20
steps: 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 - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.tag || github.ref }}
- name: Setup .NET 10 - name: Setup .NET 10
uses: actions/setup-dotnet@v4 uses: actions/setup-dotnet@v5
with: with:
dotnet-version: 10.0.x dotnet-version: 10.0.x
@@ -39,23 +60,105 @@ jobs:
Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks Expand-Archive -Force -Path dalamud.zip -DestinationPath $hooks
- name: Build (Release) - name: Build (Release)
run: dotnet build ChatTwo/ChatTwo.csproj --configuration Release run: dotnet build HellionChat/HellionChat.csproj --configuration Release
- name: Locate latest.zip - name: Locate latest.zip
id: locate id: locate
shell: pwsh shell: pwsh
run: | run: |
$zip = Get-ChildItem -Path ChatTwo\bin\Release -Recurse -Filter latest.zip | Select-Object -First 1 $zip = Get-ChildItem -Path HellionChat\bin\Release -Recurse -Filter latest.zip | Select-Object -First 1
if (-not $zip) if (-not $zip)
{ {
throw "latest.zip not found under ChatTwo\bin\Release" throw "latest.zip not found under HellionChat\bin\Release"
} }
Write-Host "Found: $($zip.FullName)" Write-Host "Found: $($zip.FullName)"
"path=$($zip.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append "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 - name: Attach to GitHub release
uses: softprops/action-gh-release@v3 uses: softprops/action-gh-release@v3
with: 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 }} files: ${{ steps.locate.outputs.path }}
body_path: release-body.md
fail_on_unmatched_files: true fail_on_unmatched_files: true
generate_release_notes: false generate_release_notes: false
+71
View File
@@ -0,0 +1,71 @@
# Code of conduct
HellionChat is a small hobby project. The contributor base is tiny and
the moderation overhead I can afford is equally small, so this document
is short and direct.
## What I expect from contributors
- Be respectful in issues, pull requests, discussions and any other
project space (Discord, email).
- Keep feedback focused on the code, the design or the documentation.
Critique the work, not the person.
- Assume good intent. People come from different backgrounds, time
zones and skill levels. A clarifying question is almost always a
better first move than an accusation.
- Stay on topic. This project is about a Dalamud chat plugin. Off-topic
arguments belong elsewhere.
- Respect that I maintain this in my spare time. Replies can take a
few days. Please do not escalate just because a thread is quiet.
## What is not welcome
- Personal attacks, slurs, doxxing, sustained disruption of threads.
- Unsolicited private contact after I have asked someone to stop.
- Sharing of private conversations without consent.
- Any content that would put other contributors or end users at risk.
## Scope
This applies to every space the project owns or that I run on its
behalf: the GitHub repository, GitHub Discussions, project-related
Discord conversations and the maintainer email address listed in
`SECURITY.md`.
It also applies when someone is identifiably representing the project
in another space, for example posting as a HellionChat maintainer in
the Dalamud Discord.
## Reporting
If something here is being broken, contact me directly. Do not open a
public issue.
- 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.
Depending on what happened and how the person responds, I will pick
the lightest measure that resolves the issue:
1. Private note asking the behaviour to stop.
2. Public correction in the affected thread.
3. Edit or removal of the offending content.
4. Temporary block from the repository or related spaces.
5. Permanent block.
Severe cases skip the lower steps. I will not negotiate over
harassment or threats.
## Acknowledgement
This document is intentionally short and project-specific rather than
a copy of a longer template. If you need a more formal reference, the
[Contributor Covenant](https://www.contributor-covenant.org/) is a
widely adopted starting point and the spirit of this document is
compatible with it.
+131
View File
@@ -0,0 +1,131 @@
# Contributing to HellionChat
Thanks for taking a look. HellionChat is a small, opinionated fork of
[Chat 2](https://github.com/Infiziert90/ChatTwo) maintained by one
person in spare time. 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: this is a
privacy-focused, EUPL-1.2-licensed Dalamud plugin that intentionally
removes the upstream webinterface and ships smaller defaults.
- Read [UPSTREAM_SYNC.md](docs/UPSTREAM_SYNC.md). Cherry-picks from upstream
Chat 2 are selective and conscious; not everything that lands there
belongs here.
- Read [SECURITY.md](SECURITY.md). Anything security-sensitive goes
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 the upstream Chat 2 strings (`Language.*.resx`) are
not handled here; they go through the upstream Chat 2 project.
- Documentation improvements (README, comments, this file).
- 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 README "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 upstream 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
[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 step only for trivial typos.
2. Fork the repository and branch off `main`. Branch naming is
informal; something like `fix/auto-tell-history-empty` or
`feat/adblock-light-mode` is plenty.
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 big one. Squash-on-merge happens at the
PR level if needed.
5. If your change touches user-visible behaviour, update the README
and/or the changelog block in `HellionChat/HellionChat.yaml` and
`repo.json` for the next version. I bump the version number myself
at release time, so you do not need to.
6. Open the pull request against `main`. The PR template will ask
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 development 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
```
dotnet restore
dotnet build HellionChat.sln -c Release
```
Tests are not part of the current `HellionChat.sln`. If you add a test
project, point it at the relevant subsystems (privacy filter,
configuration migration, message store) and mention it in the PR.
For a smoke test in-game: build, copy the output into your Dalamud
`devPlugins/HellionChat/` directory and load it through `/xlplugins`.
## Continuous integration
Every push and every pull request runs:
- `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.
## 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.
## Translations
Hellion-specific strings live in `HellionChat/Resources/HellionStrings.resx`
(English source) and `HellionStrings.<lang>.resx` (per-language).
Translations are accepted as direct pull requests against those files.
The upstream Chat 2 strings in `HellionChat/Resources/Language.*.resx` are
**not** translated in this repository. They are owned by the upstream
Chat 2 project and synced in via cherry-pick. Please contribute
upstream-string translations to
[Infiziert90/ChatTwo](https://github.com/Infiziert90/ChatTwo) instead.
## A note on response times
I respond on weekdays during European business hours and I take
weekends and FFXIV patch days off. A pull request that sits for a few
days has not been ignored; I just have not gotten to it yet. Pinging
once after a week is fine; please do not ping daily.
-100
View File
@@ -1,100 +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
icon_url: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/ChatTwo/images/icon.png
image_urls:
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/ChatTwo/images/chatWindow.png
- https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/ChatTwo/images/withSimpleTweaks.png
tags:
- Social
- UI
- Chat
- Replacement
- Privacy
changelog: |-
**Hellion Chat 0.5.4 — WrapText hardening**
Replaces the unsafe pointer-arithmetic in ImGuiUtil.WrapText with
Span- and index-based control flow. Closes the persistent CodeQL
Critical alert "unvalidated local pointer arithmetic" that kept
re-firing on every shape of the previous fix.
Hardening:
- WrapText now allocates a buffer sized by Encoding.UTF8.GetMaxByteCount
via ArrayPool, validates the actual encoded length against that
ceiling, and threads the rest of the algorithm through int offsets
instead of raw byte pointers
- Pointer arithmetic only happens inside two small private helpers
(CalcWordWrap and DrawText) that take the pinned base pointer plus
int offsets sourced from the plugin's own logic, not from any
virtual-method return
- Added a 16 KiB upper bound on the buffer rent to prevent a
pathological input from triggering an unbounded ArrayPool allocation
No user-visible behaviour change. Word-wrap output is byte-identical
to v0.5.3.
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
**Hellion Chat 0.5.3 — Pointer arithmetic hardening**
Closed CodeQL Critical alert in ImGuiUtil.WrapText by validating the
encoded byte buffer length via GetByteCount before pointer
arithmetic. Single-fix patch on top of v0.5.2.
**Hellion Chat 0.5.2 — Bugfix patch**
Auto-Tell-Tabs history-separator landed below the live tell instead
of above (preload now excludes the trigger message). Plugin icon
packaging fixed by removing a stale DalamudPackager.targets override
that conflicted with the SDK 15 default. Default config aligned to
the maintainer's daily driver: HellionThemeWindowOpacity 0.5,
Use24HourClock true, Gruppe tab no longer auto-routes /party. Two
earlier CodeQL findings closed (workflow permissions, empty-input
pointer arithmetic).
**Hellion Chat 0.5.1 — Backlog Sweep**
Pure hardening and polish. Eight backlog items from the v0.5.0
codebase review collected into one patch: cleanup-preview-stale
detection, greeted-tab dim background, Performance HelpMarker
consistency, Tabs/Database tab names from HellionStrings,
FontChooser framework-thread marshalling, async-void on
EmoteCache.LoadData, parameterised SQL via BindIntList helper.
---
Earlier history at https://github.com/JonKazama-Hellion/HellionChat/releases.
-1
View File
@@ -1 +0,0 @@
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("ChatTwo.Tests")]
+1 -7
View File
@@ -1,8 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ChatTwo", "ChatTwo\ChatTwo.csproj", "{739F75E6-B65F-41EF-9D90-F7BC519E4875}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HellionChat", "HellionChat\HellionChat.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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -14,9 +12,5 @@ Global
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{739F75E6-B65F-41EF-9D90-F7BC519E4875}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal
@@ -1,14 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.GameFunctions.Types; using HellionChat.GameFunctions.Types;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
namespace ChatTwo; namespace HellionChat;
// Hellion Chat — Auto-Tell-Tabs. // Hellion Chat — Auto-Tell-Tabs.
// //
@@ -183,6 +183,22 @@ internal sealed class AutoTellTabsService : IDisposable
return; 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); Plugin.Config.Tabs.RemoveAt(victim.Index);
// Re-anchor the active tab so the user does not silently end up on // Re-anchor the active tab so the user does not silently end up on
@@ -207,6 +223,17 @@ internal sealed class AutoTellTabsService : IDisposable
PreloadHistory(tab, partner.Name, partner.World, currentMessage.Id); PreloadHistory(tab, partner.Name, partner.World, currentMessage.Id);
tab.AddMessage(currentMessage, unread: true); 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); Plugin.Config.Tabs.Add(tab);
} }
@@ -355,6 +382,27 @@ internal sealed class AutoTellTabsService : IDisposable
var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count; var lastIndexValid = lastIndex >= 0 && lastIndex < Plugin.Config.Tabs.Count;
var currentWasTempTab = lastIndexValid && Plugin.Config.Tabs[lastIndex].IsTempTab; 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); Plugin.Config.Tabs.RemoveAll(t => t.IsTempTab);
// Force a switch to tab 0 if the active tab was a temp tab OR // 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 Dalamud.Game.Text.SeStringHandling;
using MessagePack; using MessagePack;
namespace ChatTwo; namespace HellionChat;
[Union(0, typeof(TextChunk))] [Union(0, typeof(TextChunk))]
[Union(1, typeof(IconChunk))] [Union(1, typeof(IconChunk))]
@@ -1,6 +1,6 @@
using Dalamud.Game.Text; using Dalamud.Game.Text;
namespace ChatTwo.Code; namespace HellionChat.Code;
public class ChatCode public class ChatCode
{ {
@@ -91,13 +91,10 @@ public class ChatCode
public override bool Equals(object? obj) public override bool Equals(object? obj)
{ {
if (obj == null)
return false;
if (obj is not ChatCode code) if (obj is not ChatCode code)
return false; return false;
return GetHashCode() == code.GetHashCode(); return Type == code.Type && Source == code.Source && Target == code.Target;
} }
public override int GetHashCode() public override int GetHashCode()
@@ -1,6 +1,6 @@
using Dalamud.Game.Text; using Dalamud.Game.Text;
namespace ChatTwo.Code; namespace HellionChat.Code;
[Flags] [Flags]
public enum ChatSource : ushort public enum ChatSource : ushort
@@ -1,6 +1,6 @@
using ChatTwo.Resources; using HellionChat.Resources;
namespace ChatTwo.Code; namespace HellionChat.Code;
internal static class ChatSourceExt internal static class ChatSourceExt
{ {
@@ -1,4 +1,4 @@
namespace ChatTwo.Code; namespace HellionChat.Code;
public enum ChatType : ushort public enum ChatType : ushort
{ {
@@ -1,8 +1,8 @@
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Config; using Dalamud.Game.Config;
namespace ChatTwo.Code; namespace HellionChat.Code;
internal static class ChatTypeExt internal static class ChatTypeExt
{ {
@@ -1,4 +1,4 @@
namespace ChatTwo.Code; namespace HellionChat.Code;
public enum InputChannel : uint public enum InputChannel : uint
{ {
@@ -1,6 +1,6 @@
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
namespace ChatTwo.Code; namespace HellionChat.Code;
internal static class InputChannelExt internal static class InputChannelExt
{ {
@@ -1,6 +1,6 @@
using Dalamud.Game.Command; using Dalamud.Game.Command;
namespace ChatTwo; namespace HellionChat;
internal sealed class Commands : IDisposable internal sealed class Commands : IDisposable
{ {
@@ -1,8 +1,8 @@
using System.Collections; using System.Collections;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.GameFunctions.Types; using HellionChat.GameFunctions.Types;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud; using Dalamud;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
@@ -10,7 +10,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.FontIdentifier;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo; namespace HellionChat;
[Serializable] [Serializable]
public class ConfigKeyBind public class ConfigKeyBind
@@ -34,7 +34,7 @@ public class ConfigKeyBind
[Serializable] [Serializable]
public class Configuration : IPluginConfiguration public class Configuration : IPluginConfiguration
{ {
private const int LatestVersion = 10; private const int LatestVersion = 12;
public int Version { get; set; } = LatestVersion; public int Version { get; set; } = LatestVersion;
@@ -103,6 +103,33 @@ public class Configuration : IPluginConfiguration
// want the auto tabs themselves without the extra UI affordance. // want the auto tabs themselves without the extra UI affordance.
public bool AutoTellTabsShowGreetedToggle; 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) public int GetRetentionDays(ChatType type)
{ {
if (RetentionPerChannelDays.TryGetValue(type, out var userOverride)) if (RetentionPerChannelDays.TryGetValue(type, out var userOverride))
@@ -118,6 +145,7 @@ public class Configuration : IPluginConfiguration
public bool HideWhenUiHidden = true; public bool HideWhenUiHidden = true;
public bool HideInLoadingScreens; public bool HideInLoadingScreens;
public bool HideInBattle; public bool HideInBattle;
public bool HideInNewGamePlusMenu;
public bool HideWhenInactive; public bool HideWhenInactive;
public int InactivityHideTimeout = 10; public int InactivityHideTimeout = 10;
public bool InactivityHideActiveDuringBattle = true; public bool InactivityHideActiveDuringBattle = true;
@@ -194,6 +222,7 @@ public class Configuration : IPluginConfiguration
public float TooltipOffset; public float TooltipOffset;
public float WindowAlpha = 100f; public float WindowAlpha = 100f;
public Dictionary<ChatType, uint> ChatColours = new(); public Dictionary<ChatType, uint> ChatColours = new();
public bool ColorSelectedInputChannelButton = true;
public List<Tab> Tabs = []; public List<Tab> Tabs = [];
public bool OverrideStyle; public bool OverrideStyle;
@@ -214,6 +243,7 @@ public class Configuration : IPluginConfiguration
HideWhenUiHidden = other.HideWhenUiHidden; HideWhenUiHidden = other.HideWhenUiHidden;
HideInLoadingScreens = other.HideInLoadingScreens; HideInLoadingScreens = other.HideInLoadingScreens;
HideInBattle = other.HideInBattle; HideInBattle = other.HideInBattle;
HideInNewGamePlusMenu = other.HideInNewGamePlusMenu;
HideWhenInactive = other.HideWhenInactive; HideWhenInactive = other.HideWhenInactive;
InactivityHideTimeout = other.InactivityHideTimeout; InactivityHideTimeout = other.InactivityHideTimeout;
InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle; InactivityHideActiveDuringBattle = other.InactivityHideActiveDuringBattle;
@@ -261,6 +291,7 @@ public class Configuration : IPluginConfiguration
TooltipOffset = other.TooltipOffset; TooltipOffset = other.TooltipOffset;
WindowAlpha = other.WindowAlpha; WindowAlpha = other.WindowAlpha;
ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value); ChatColours = other.ChatColours.ToDictionary(entry => entry.Key, entry => entry.Value);
ColorSelectedInputChannelButton = other.ColorSelectedInputChannelButton;
// Hellion Chat — Auto-Tell-Tabs are session-only and therefore // Hellion Chat — Auto-Tell-Tabs are session-only and therefore
// never present in a disk-loaded copy. Keep the live temp tabs of // never present in a disk-loaded copy. Keep the live temp tabs of
@@ -296,6 +327,11 @@ public class Configuration : IPluginConfiguration
AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay; AutoTellTabsCompactDisplay = other.AutoTellTabsCompactDisplay;
AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload; AutoTellTabsHistoryPreload = other.AutoTellTabsHistoryPreload;
AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle; AutoTellTabsShowGreetedToggle = other.AutoTellTabsShowGreetedToggle;
SeenPopOutInputHint = other.SeenPopOutInputHint;
PopOutInputEnabled = other.PopOutInputEnabled;
SeenPopOutHeaderHint = other.SeenPopOutHeaderHint;
AutoTellTabsOpenAsPopout = other.AutoTellTabsOpenAsPopout;
} }
} }
@@ -8,7 +8,7 @@ using Dalamud.Bindings.ImGui;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
namespace ChatTwo; namespace HellionChat;
public static class EmoteCache public static class EmoteCache
{ {
@@ -192,7 +192,7 @@ public static class EmoteCache
} }
else else
{ {
var content = await new HttpClient().GetAsync(EmotePath.Format(emote.Id)); var content = await Client.GetAsync(EmotePath.Format(emote.Id));
RawData = await content.Content.ReadAsByteArrayAsync(); RawData = await content.Content.ReadAsByteArrayAsync();
await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read); await using var stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Read);
@@ -1,8 +1,8 @@
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using ChatTwo.Code; using HellionChat.Code;
namespace ChatTwo.Export; namespace HellionChat.Export;
internal enum ExportFormat internal enum ExportFormat
{ {
@@ -6,7 +6,7 @@ using Dalamud.Interface.ManagedFontAtlas;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo; namespace HellionChat;
public class FontManager public class FontManager
{ {
@@ -39,11 +39,18 @@ public class FontManager
} }
else else
{ {
GameSymFont = new HttpClient().GetAsync("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf") // Dispose HttpClient and HttpResponseMessage to avoid socket
.Result // exhaustion on repeated cold-start downloads. GetAwaiter().GetResult()
.Content // unwraps AggregateException so failures surface cleanly. A full
.ReadAsByteArrayAsync() // async refactor of the constructor would be cleaner but is out of
.Result; // scope for v1.0.0 — tracked in the backlog.
using var client = new HttpClient();
using var response = client
.GetAsync("https://img.finalfantasyxiv.com/lds/pc/global/fonts/FFXIV_Lodestone_SSF.ttf")
.GetAwaiter()
.GetResult();
response.EnsureSuccessStatusCode();
GameSymFont = response.Content.ReadAsByteArrayAsync().GetAwaiter().GetResult();
Dalamud.Utility.FilesystemUtil.WriteAllBytesSafe(filePath, GameSymFont); Dalamud.Utility.FilesystemUtil.WriteAllBytesSafe(filePath, GameSymFont);
} }
@@ -1,8 +1,8 @@
using System.Text; using System.Text;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.GameFunctions.Types; using HellionChat.GameFunctions.Types;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Config; using Dalamud.Game.Config;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Hooking; using Dalamud.Hooking;
@@ -22,7 +22,7 @@ using Lumina.Text.ReadOnly;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
namespace ChatTwo.GameFunctions; namespace HellionChat.GameFunctions;
internal sealed unsafe class Chat : IDisposable internal sealed unsafe class Chat : IDisposable
{ {
@@ -1,10 +1,10 @@
using System.Text; using System.Text;
using ChatTwo.Resources; using HellionChat.Resources;
using Dalamud.Memory; using Dalamud.Memory;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
namespace ChatTwo.GameFunctions; namespace HellionChat.GameFunctions;
public unsafe class ChatBox 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.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Client.UI.Info;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Misc;
namespace ChatTwo.GameFunctions; namespace HellionChat.GameFunctions;
internal sealed unsafe class Context internal sealed unsafe class Context
{ {
@@ -16,10 +16,12 @@ using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
namespace ChatTwo.GameFunctions; namespace HellionChat.GameFunctions;
internal unsafe class GameFunctions : IDisposable internal unsafe class GameFunctions : IDisposable
{ {
internal const string NewGamePlusAddonName = "QuestRedo";
#region Hooks #region Hooks
[Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))] [Signature("E8 ?? ?? ?? ?? 48 85 C0 0F 84 ?? ?? ?? ?? 48 8B D0 49 8D 4F", DetourName = nameof(ResolveTextCommandPlaceholderDetour))]
private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!; private Hook<ResolveTextCommandPlaceholderDelegate>? ResolveTextCommandPlaceholderHook = null!;
@@ -249,9 +251,15 @@ internal unsafe class GameFunctions : IDisposable
private nint ResolveTextCommandPlaceholderDetour(nint a1, byte* placeholderText, byte a3, byte a4) 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); var placeholder = MemoryHelper.ReadStringNullTerminated((nint) placeholderText);
if (ReplacementName == null || placeholder != Placeholder) if (ReplacementName == null || placeholder != Placeholder)
return ResolveTextCommandPlaceholderHook!.Original(a1, placeholderText, a3, a4); return ResolveTextCommandPlaceholderHook.Original(a1, placeholderText, a3, a4);
MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName); MemoryHelper.WriteString(PlaceholderNamePtr, ReplacementName);
ReplacementName = null; ReplacementName = null;
@@ -1,16 +1,16 @@
using System.Numerics; using System.Numerics;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.GameFunctions.Types; using HellionChat.GameFunctions.Types;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using Dalamud.Game.Config; using Dalamud.Game.Config;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using Dalamud.Bindings.ImGui; 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 { internal enum KeyboardSource {
Game, Game,
@@ -414,13 +414,13 @@ internal unsafe class KeybindManager : IDisposable {
if (ConfigKeybindPressed(source, Plugin.Config.ChatTabForward)) if (ConfigKeybindPressed(source, Plugin.Config.ChatTabForward))
{ {
Plugin.KeyState[Plugin.Config.ChatTabForward!.Key] = false; Plugin.KeyState[Plugin.Config.ChatTabForward!.Key] = false;
Plugin.ChatLogWindow.ChangeTabDelta(1); DispatchTabDelta(1);
return; return;
} }
if (ConfigKeybindPressed(source, Plugin.Config.ChatTabBackward)) if (ConfigKeybindPressed(source, Plugin.Config.ChatTabBackward))
{ {
Plugin.KeyState[Plugin.Config.ChatTabBackward!.Key] = false; Plugin.KeyState[Plugin.Config.ChatTabBackward!.Key] = false;
Plugin.ChatLogWindow.ChangeTabDelta(-1); DispatchTabDelta(-1);
return; 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) private static Keybind GetKeybind(string id)
{ {
var outData = new FFXIVClientStructs.FFXIV.Client.System.Input.Keybind(); var outData = new FFXIVClientStructs.FFXIV.Client.System.Input.Keybind();
@@ -1,10 +1,10 @@
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Info; using FFXIVClientStructs.FFXIV.Client.UI.Info;
namespace ChatTwo.GameFunctions; namespace HellionChat.GameFunctions;
internal static unsafe class Party 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 class ChannelSwitchInfo {
internal InputChannel? Channel { get; } internal InputChannel? Channel { get; }
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types; namespace HellionChat.GameFunctions.Types;
internal sealed class ChatActivatedArgs internal sealed class ChatActivatedArgs
{ {
@@ -1,6 +1,6 @@
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
namespace ChatTwo.GameFunctions.Types; namespace HellionChat.GameFunctions.Types;
internal class Keybind internal class Keybind
{ {
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types; namespace HellionChat.GameFunctions.Types;
[Flags] [Flags]
public enum ModifierFlag public enum ModifierFlag
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types; namespace HellionChat.GameFunctions.Types;
internal enum RotateMode internal enum RotateMode
{ {
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types; namespace HellionChat.GameFunctions.Types;
internal sealed class TellHistoryInfo internal sealed class TellHistoryInfo
{ {
@@ -1,4 +1,4 @@
namespace ChatTwo.GameFunctions.Types; namespace HellionChat.GameFunctions.Types;
public enum TellReason public enum TellReason
{ {
@@ -1,7 +1,7 @@
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Character;
namespace ChatTwo.GameFunctions.Types; namespace HellionChat.GameFunctions.Types;
[Serializable] [Serializable]
public class TellTarget public class TellTarget
@@ -30,6 +30,9 @@ public class TellTarget
public unsafe void FromTarget(IPlayerCharacter target) public unsafe void FromTarget(IPlayerCharacter target)
{ {
if (target.Address == nint.Zero)
return;
Name = target.Name.TextValue; Name = target.Name.TextValue;
World = target.HomeWorld.RowId; World = target.HomeWorld.RowId;
ContentId = ((Character*)target.Address)->ContentId; ContentId = ((Character*)target.Address)->ContentId;
@@ -4,22 +4,31 @@
0.1.0 is our bootstrap release; the underlying Chat 2 base is 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 called out in the yaml changelog so users can see what it
derives from. --> derives from. -->
<Version>0.5.4</Version> <Version>1.0.3</Version>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<!-- HellionChat fork: assembly is renamed so Dalamud uses <!-- Honor packages.lock.json on restore so floating version ranges
pluginConfigs/HellionChat instead of pluginConfigs/ChatTwo, don't silently drift between machines or CI runs. -->
keeping our state independent from the upstream plugin. <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
Code namespace stays ChatTwo.* so upstream cherry-picks <!-- v1.0.0 standalone cut — both AssemblyName and RootNamespace
apply cleanly. --> 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> <AssemblyName>HellionChat</AssemblyName>
<RootNamespace>ChatTwo</RootNamespace> <RootNamespace>HellionChat</RootNamespace>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.4" /> <PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" /> <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="morelinq" Version="4.4.0" />
<PackageReference Include="Pidgin" Version="3.3.0" /> <PackageReference Include="Pidgin" Version="3.5.1" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
</ItemGroup> </ItemGroup>
+237
View File
@@ -0,0 +1,237 @@
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
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/withSimpleTweaks.png
tags:
- Social
- UI
- Chat
- Replacement
- Privacy
changelog: |-
**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; using Dalamud.Plugin.Ipc;
namespace ChatTwo.Ipc; namespace HellionChat.Ipc;
public sealed class ExtraChat : IDisposable public sealed class ExtraChat : IDisposable
{ {
@@ -49,6 +49,8 @@ public sealed class ExtraChat : IDisposable
public void Dispose() public void Dispose()
{ {
OverrideChannelGate.Unsubscribe(OnOverrideChannel); OverrideChannelGate.Unsubscribe(OnOverrideChannel);
ChannelCommandColoursGate.Unsubscribe(OnChannelCommandColours);
ChannelNamesGate.Unsubscribe(OnChannelNames);
} }
private void OnOverrideChannel(OverrideInfo info) private void OnOverrideChannel(OverrideInfo info)
@@ -1,7 +1,7 @@
using ChatTwo.Code; using HellionChat.Code;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
namespace ChatTwo.Ipc; namespace HellionChat.Ipc;
using ChatInputState = (bool InputVisible, bool InputFocused, bool HasText, bool IsTyping, int TextLength, ChatType ChannelType); 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; Plugin = plugin;
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>("ChatTwo.GetChatInputState"); StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>("HellionChat.GetChatInputState");
StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>("ChatTwo.ChatInputStateChanged"); StateChangedGate = Plugin.Interface.GetIpcProvider<ChatInputState, object?>("HellionChat.ChatInputStateChanged");
StateQueryGate.RegisterFunc(GetState); StateQueryGate.RegisterFunc(GetState);
} }
@@ -2,7 +2,7 @@ using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
namespace ChatTwo; namespace HellionChat;
internal sealed class IpcManager : IDisposable internal sealed class IpcManager : IDisposable
{ {
@@ -15,15 +15,15 @@ internal sealed class IpcManager : IDisposable
public IpcManager() public IpcManager()
{ {
RegisterGate = Plugin.Interface.GetIpcProvider<string>("ChatTwo.Register"); RegisterGate = Plugin.Interface.GetIpcProvider<string>("HellionChat.Register");
RegisterGate.RegisterFunc(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); 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(); AvailableGate.SendMessage();
} }
@@ -47,7 +47,7 @@ internal sealed class IpcManager : IDisposable
public void Dispose() public void Dispose()
{ {
UnregisterGate.UnregisterFunc(); UnregisterGate.UnregisterAction();
RegisterGate.UnregisterFunc(); RegisterGate.UnregisterFunc();
Registered.Clear(); Registered.Clear();
} }
@@ -1,6 +1,6 @@
using System.Text; using System.Text;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@@ -8,7 +8,7 @@ using Dalamud.Game.Text;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace ChatTwo; namespace HellionChat;
public partial class Message public partial class Message
{ {
@@ -49,7 +49,10 @@ public partial class Message
ExtraChatChannel = extraChatChannel; ExtraChatChannel = extraChatChannel;
Hash = GenerateHash(); 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; chunk.Message = this;
} }
@@ -1,9 +1,9 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Diagnostics; using System.Diagnostics;
using System.Text; using System.Text;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Chat; using Dalamud.Game.Chat;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
@@ -15,7 +15,7 @@ using Lumina.Text.Expressions;
using Lumina.Text.Payloads; using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
namespace ChatTwo; namespace HellionChat;
internal class MessageManager : IAsyncDisposable internal class MessageManager : IAsyncDisposable
{ {
@@ -1,9 +1,9 @@
using System.Buffers; using System.Buffers;
using System.Collections; using System.Collections;
using System.Data.Common; using System.Data.Common;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Ui; using HellionChat.Ui;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using MessagePack; using MessagePack;
using MessagePack.Formatters; using MessagePack.Formatters;
@@ -13,7 +13,7 @@ using Microsoft.Data.Sqlite;
using DalamudUtil = Dalamud.Utility.Util; using DalamudUtil = Dalamud.Utility.Util;
using Encoding = System.Text.Encoding; using Encoding = System.Text.Encoding;
namespace ChatTwo; namespace HellionChat;
internal static class DbExtensions internal static class DbExtensions
{ {
@@ -1,8 +1,8 @@
using System.Numerics; using System.Numerics;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Ui; using HellionChat.Ui;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
@@ -22,13 +22,13 @@ using Dalamud.Bindings.ImGui;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Action = System.Action; using Action = System.Action;
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload; 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 public sealed class PayloadHandler
{ {
private const string PopupId = "chat2-context-popup"; private const string PopupId = "hellionchat-context-popup";
private ChatLogWindow LogWindow { get; } private ChatLogWindow LogWindow { get; }
private (Chunk, Payload?)? Popup { get; set; } private (Chunk, Payload?)? Popup { get; set; }
@@ -332,10 +332,19 @@ public sealed class PayloadHandler
atkBase->SetPosition((short) x, (short) y); atkBase->SetPosition((short) x, (short) y);
} }
private const float MaxInlineIconSize = 32f;
private static void InlineIcon(IDalamudTextureWrap icon) 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 cursor = ImGui.GetCursorPos();
var size = ImGuiHelpers.ScaledVector2(32, 32);
ImGui.Image(icon.Handle, size); ImGui.Image(icon.Handle, size);
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing())); ImGui.SetCursorPos(cursor + new Vector2(size.X + 4, size.Y - ImGui.GetTextLineHeightWithSpacing()));
+108 -14
View File
@@ -2,10 +2,10 @@ using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using ChatTwo.Ipc; using HellionChat.Ipc;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Ui; using HellionChat.Ui;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.IoC; using Dalamud.IoC;
@@ -14,7 +14,7 @@ using Dalamud.Plugin.Services;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.ImGuiFileDialog; using Dalamud.Interface.ImGuiFileDialog;
namespace ChatTwo; namespace HellionChat;
// ReSharper disable once ClassNeverInstantiated.Global // ReSharper disable once ClassNeverInstantiated.Global
public sealed class Plugin : IDalamudPlugin public sealed class Plugin : IDalamudPlugin
@@ -94,6 +94,12 @@ public sealed class Plugin : IDalamudPlugin
public Plugin() 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 try
{ {
GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime(); GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime();
@@ -152,22 +158,100 @@ public sealed class Plugin : IDalamudPlugin
}); });
} }
// Hellion default tab layout for first-run and v10-wipe. // Hellion Chat v10 → v11 — adds the global Configuration.PopOutInputEnabled
// General catches player chat plus active gameplay events; the // master switch and SeenPopOutInputHint flag for the v0.6.0 pop-out
// System tab takes the technical noise so it does not bury real // input feature. Lightweight migration: defaults both fields,
// conversation. Beginner tab only appears when the Novice // no user-facing notification because the change is opt-in only.
// Network is enabled in Audio and Notifications, otherwise it if (Config.Version < 11)
// would just sit empty. {
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.SettingsRefactor_Migration_Title,
Content = HellionStrings.SettingsRefactor_Migration_Content,
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
InitialDuration = TimeSpan.FromSeconds(25),
});
}
// 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) if (Config.Tabs.Count == 0)
{ {
Config.Tabs.Add(TabsUtil.VanillaGeneral); Config.Tabs.Add(TabsUtil.VanillaGeneral);
Config.Tabs.Add(TabsUtil.HellionSystem); Config.Tabs.Add(TabsUtil.HellionSystem);
Config.Tabs.Add(TabsUtil.HellionFreeCompany); Config.Tabs.Add(TabsUtil.HellionFreeCompany);
Config.Tabs.Add(TabsUtil.HellionParty); Config.Tabs.Add(TabsUtil.HellionParty);
if (Config.ShowNoviceNetwork)
Config.Tabs.Add(TabsUtil.HellionBeginner);
Config.Tabs.Add(TabsUtil.HellionLinkshell); Config.Tabs.Add(TabsUtil.HellionLinkshell);
Config.Tabs.Add(TabsUtil.VanillaTellExclusive);
} }
LanguageChanged(Interface.UiLanguage); LanguageChanged(Interface.UiLanguage);
@@ -487,6 +571,16 @@ public sealed class Plugin : IDalamudPlugin
return; 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(); ChatLogWindow.HideStateCheck();
Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden; Interface.UiBuilder.DisableUserUiHide = !Config.HideWhenUiHidden;
@@ -1,6 +1,6 @@
using ChatTwo.Code; using HellionChat.Code;
namespace ChatTwo.Privacy; namespace HellionChat.Privacy;
internal static class PrivacyDefaults 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),
};
}
}
@@ -8,7 +8,7 @@
#nullable enable #nullable enable
namespace ChatTwo.Resources; namespace HellionChat.Resources;
[global::System.Diagnostics.DebuggerNonUserCodeAttribute] [global::System.Diagnostics.DebuggerNonUserCodeAttribute]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
@@ -26,7 +26,7 @@ internal class HellionStrings
get get
{ {
if (resourceMan is null) if (resourceMan is null)
resourceMan = new global::System.Resources.ResourceManager("ChatTwo.Resources.HellionStrings", typeof(HellionStrings).Assembly); resourceMan = new global::System.Resources.ResourceManager("HellionChat.Resources.HellionStrings", typeof(HellionStrings).Assembly);
return resourceMan; return resourceMan;
} }
} }
@@ -179,6 +179,8 @@ internal class HellionStrings
internal static string ChatLog_AutoTellTabs_Compact_Description => Get(nameof(ChatLog_AutoTellTabs_Compact_Description)); 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_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_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_PreloadHint => Get(nameof(ChatLog_AutoTellTabs_PreloadHint));
internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint)); internal static string ChatLog_AutoTellTabs_ConflictHint => Get(nameof(ChatLog_AutoTellTabs_ConflictHint));
@@ -242,4 +244,39 @@ internal class HellionStrings
internal static string Tabs_Presets_Beginner => Get(nameof(Tabs_Presets_Beginner)); 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 => Get(nameof(Tabs_Presets_Linkshell));
internal static string Tabs_Presets_Linkshell_Hint => Get(nameof(Tabs_Presets_Linkshell_Hint)); internal static string Tabs_Presets_Linkshell_Hint => Get(nameof(Tabs_Presets_Linkshell_Hint));
// 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));
} }
@@ -19,7 +19,7 @@
<value>Datenschutz-Filter aktivieren</value> <value>Datenschutz-Filter aktivieren</value>
</data> </data>
<data name="Privacy_FilterEnabled_Description" xml:space="preserve"> <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>
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve"> <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> <value>Der Filter steuert nur, was in die lokale Datenbank geschrieben wird. Im Chat-Log siehst du weiterhin jede Nachricht live, ausgeschlossene Kanäle werden nur nicht mehr gespeichert. Wenn du Kanäle auch aus der sichtbaren Anzeige entfernen willst, nutze die normalen Chat-Tab-Filter im Spiel.</value>
@@ -211,7 +211,7 @@
<value>Volle Historie</value> <value>Volle Historie</value>
</data> </data>
<data name="Wizard_Profile_FullHistory_Description" xml:space="preserve"> <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>
<data name="Wizard_Profile_FullHistory_GdprWarning" xml:space="preserve"> <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> <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>
@@ -406,6 +406,12 @@
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve"> <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> <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>
<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"> <data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
<value>Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen.</value> <value>Die Anzahl der vorgeladenen Tells lässt sich im Datenschutz-Tab einstellen.</value>
</data> </data>
@@ -555,4 +561,67 @@
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve"> <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> <value>Wenn du mehrere Linkshells benutzt, empfiehlt der Maintainer einen Tab pro Shell für eine sauberere Übersicht. Tab duplizieren und je Kopie die Kanalauswahl einschränken.</value>
</data> </data>
<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>
</root> </root>
@@ -19,7 +19,7 @@
<value>Enable privacy filter</value> <value>Enable privacy filter</value>
</data> </data>
<data name="Privacy_FilterEnabled_Description" xml:space="preserve"> <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>
<data name="Privacy_FilterEnabled_StorageOnly_Help" xml:space="preserve"> <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> <value>The filter only controls what is written to the local database. The chat log itself keeps showing every message live, disallowed channels just stop being saved. Use the channel hide options in your in-game chat tabs if you want to remove channels from the visible chat.</value>
@@ -211,7 +211,7 @@
<value>Full History</value> <value>Full History</value>
</data> </data>
<data name="Wizard_Profile_FullHistory_Description" xml:space="preserve"> <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>
<data name="Wizard_Profile_FullHistory_GdprWarning" xml:space="preserve"> <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> <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>
@@ -406,6 +406,12 @@
<data name="ChatLog_AutoTellTabs_GreetedToggle_Description" xml:space="preserve"> <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> <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>
<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"> <data name="ChatLog_AutoTellTabs_PreloadHint" xml:space="preserve">
<value>The number of preloaded tells is configured in the Privacy tab.</value> <value>The number of preloaded tells is configured in the Privacy tab.</value>
</data> </data>
@@ -555,4 +561,67 @@
<data name="Tabs_Presets_Linkshell_Hint" xml:space="preserve"> <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> <value>If you use multiple linkshells, the maintainer recommends one tab per shell for cleaner readability. Duplicate this tab and narrow the channel selection per copy.</value>
</data> </data>
<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>
</root> </root>
@@ -7,7 +7,7 @@
// </auto-generated> // </auto-generated>
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
namespace ChatTwo.Resources { namespace HellionChat.Resources {
using System; using System;
@@ -38,7 +38,7 @@ namespace ChatTwo.Resources {
internal static global::System.Resources.ResourceManager ResourceManager { internal static global::System.Resources.ResourceManager ResourceManager {
get { get {
if (object.ReferenceEquals(resourceMan, null)) { 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; resourceMan = temp;
} }
return resourceMan; 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> /// <summary>
/// Looks up a localized string similar to Chat colours. /// Looks up a localized string similar to Chat colours.
/// </summary> /// </summary>
@@ -2661,6 +2679,24 @@ namespace ChatTwo.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again..
/// </summary>
internal static string Options_HideInNewGamePlusMenu_Description {
get {
return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Description", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Hide while New Game+ menu is open.
/// </summary>
internal static string Options_HideInNewGamePlusMenu_Name {
get {
return ResourceManager.GetString("Options_HideInNewGamePlusMenu_Name", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Hide {0} during loading screens.. /// Looks up a localized string similar to Hide {0} during loading screens..
/// </summary> /// </summary>
@@ -208,6 +208,12 @@
<data name="Options_ChatColours_Import"> <data name="Options_ChatColours_Import">
<value>Vom Spiel importieren</value> <value>Vom Spiel importieren</value>
</data> </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"> <data name="Options_Tabs_Tab">
<value>Kanäle</value> <value>Kanäle</value>
</data> </data>
@@ -1190,6 +1196,12 @@ Sie wurden gewarnt.</value>
<data name="Options_HideInBattle_Description" xml:space="preserve"> <data name="Options_HideInBattle_Description" xml:space="preserve">
<value>Blende den Chat während der Kämpfe aus.</value> <value>Blende den Chat während der Kämpfe aus.</value>
</data> </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"> <data name="Options_Emote_EmoteStats" xml:space="preserve">
<value>Emote-Statistik</value> <value>Emote-Statistik</value>
</data> </data>
@@ -208,6 +208,12 @@
<data name="Options_ChatColours_Import"> <data name="Options_ChatColours_Import">
<value>Import from game</value> <value>Import from game</value>
</data> </data>
<data name="Options_ColorSelectedInputChannelButton_Name" xml:space="preserve">
<value>Tint channel selector with channel colour</value>
</data>
<data name="Options_ColorSelectedInputChannelButton_Description" xml:space="preserve">
<value>The channel selector button next to the input field is tinted with the currently active channel's colour. Matches the tinting of the input text itself.</value>
</data>
<data name="Options_Tabs_Tab"> <data name="Options_Tabs_Tab">
<value>Tabs</value> <value>Tabs</value>
</data> </data>
@@ -1189,6 +1195,12 @@
<data name="Options_HideInBattle_Description" xml:space="preserve"> <data name="Options_HideInBattle_Description" xml:space="preserve">
<value>Hide the chat during battles.</value> <value>Hide the chat during battles.</value>
</data> </data>
<data name="Options_HideInNewGamePlusMenu_Name" xml:space="preserve">
<value>Hide while New Game+ menu is open</value>
</data>
<data name="Options_HideInNewGamePlusMenu_Description" xml:space="preserve">
<value>Hide the chat while the New Game+ menu is open. Closing the menu shows the chat again.</value>
</data>
<data name="Options_Emote_EmoteStats" xml:space="preserve"> <data name="Options_Emote_EmoteStats" xml:space="preserve">
<value>Emote Stats</value> <value>Emote Stats</value>
</data> </data>
@@ -1364,7 +1364,7 @@
<value>启动</value> <value>启动</value>
</data> </data>
<data name="Webinterface_Start_Success" xml:space="preserve"> <data name="Webinterface_Start_Success" xml:space="preserve">
<value>网页界面已停止。</value> <value>网页界面已启动。</value>
</data> </data>
<data name="Webinterface_Start_Failed" xml:space="preserve"> <data name="Webinterface_Start_Failed" xml:space="preserve">
<value>网页界面启动失败。检查 /xllog 获取更多信息。</value> <value>网页界面启动失败。检查 /xllog 获取更多信息。</value>
+3 -3
View File
@@ -2,7 +2,7 @@
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
namespace ChatTwo; namespace HellionChat;
public static class Sheets public static class Sheets
{ {
@@ -37,7 +37,7 @@ public static class Sheets
public static IEnumerable<World> WorldsOnDatacenter(IPlayerCharacter character) public static IEnumerable<World> WorldsOnDatacenter(IPlayerCharacter character)
{ {
var dcRow = character.HomeWorld.Value.DataCenter.Value.Region.RowId; var dcRow = character.HomeWorld.Value.DataCenter.RowId;
return WorldSheet.Where(world => world.IsPublic && world.DataCenter.Value.Region.RowId == dcRow); return WorldSheet.Where(world => world.IsPublic && world.DataCenter.RowId == dcRow);
} }
} }
@@ -1,4 +1,4 @@
namespace ChatTwo.Ui; namespace HellionChat.Ui;
internal class AutoCompleteInfo internal class AutoCompleteInfo
{ {
+246
View File
@@ -0,0 +1,246 @@
using System;
using System.Numerics;
using HellionChat.Code;
using HellionChat.Util;
using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii;
namespace HellionChat.Ui;
// Hellion Chat — v0.6.0 input bar component for pop-out windows.
//
// Pragmatischer Refactor-Scope: Render() ist ein leerer Marker-Stub für
// das Hauptfenster — der bestehende Input-Layer in ChatLogWindow bleibt
// unangetastet, weil ein 400-Zeilen-Extract aus einem 1926-Zeilen-File
// das v0.6.0-Risiko unverhältnismäßig erhöhen würde. Pop-Outs nutzen
// ausschließlich RenderCompact(), das ist der ganze v0.6.0-Mehrwert.
// Sollte das Hauptfenster selber später eine Compact-Variante brauchen
// (oder das große Extract sich aus anderem Grund lohnen), kann Render()
// in einem späteren Cycle gefüllt werden.
public sealed class ChatInputBar
{
private readonly Plugin _plugin;
private readonly ChatLogWindow _host;
private readonly Func<Tab?> _activeTabAccessor;
private readonly InputState _state = new();
public ChatInputBar(Plugin plugin, ChatLogWindow host, Func<Tab?> activeTabAccessor)
{
_plugin = plugin;
_host = host;
_activeTabAccessor = activeTabAccessor;
}
public InputState State => _state;
public bool IsFocused { get; private set; }
// Stub. v0.6.0 belässt den Hauptfenster-Input wie er ist.
public void Render()
{
}
// Compact rendering for pop-out windows.
//
// v0.6.0 Compact-Layout: Channel-Icon-Button links (Background-Farbe
// aus ChatColours), Text-Input rechts daneben. Auto-Translate-Picker
// ist bewusst NICHT im Compact-Mode (Spec-Abweichung Layout D → A).
// Rechtfertigung: das Hauptfenster-Auto-Complete-Popup ist nicht ohne
// grossen Refactor pro Window instanzierbar; typische Pop-Out-Use-Cases
// (FC-Greeter, Club-Hostess) brauchen Auto-Translate selten dort.
// Eigene Compact-Auto-Complete-Implementation kann ein späterer
// Cycle nachreichen wenn Tester-Feedback das verlangt.
//
// Channel-Switch wirkt via Plugin.Functions.Chat global (FFXIV-API).
// Pro Pop-Out unabhängig bleiben Text-Buffer und History-Cursor.
public void RenderCompact()
{
var tab = _activeTabAccessor();
if (tab == null)
return;
DrawChannelIconButton(tab);
ImGui.SameLine();
DrawCompactInput(tab);
}
private void DrawCompactInput(Tab tab)
{
// Input takes the whole remaining width — no auto-translate button
// reserved on the right side in v0.6.0 (see RenderCompact comment).
var inputWidth = ImGui.GetContentRegionAvail().X;
if (inputWidth < 60f)
inputWidth = 60f;
ImGui.SetNextItemWidth(inputWidth);
// CallbackHistory wires up Up/Down navigation against the shared
// InputHistoryService. Submit is detected the same way the main
// window does it: via IsItemDeactivated + Enter, NOT EnterReturnsTrue
// (matching v0.5.x ChatLogWindow.cs behavior).
const ImGuiInputTextFlags flags = ImGuiInputTextFlags.CallbackHistory;
ImGui.InputText($"##chat-compact-input-{tab.Identifier}", ref _state.Buffer, 500, flags, CompactCallback);
IsFocused = ImGui.IsItemActive();
if (ImGui.IsItemDeactivated()
&& (ImGui.IsKeyDown(ImGuiKey.Enter) || ImGui.IsKeyDown(ImGuiKey.KeypadEnter)))
{
SubmitCompact(tab);
}
}
private void SubmitCompact(Tab tab)
{
if (string.IsNullOrWhiteSpace(_state.Buffer))
return;
var text = _state.Buffer;
_state.Buffer = string.Empty;
_state.HistoryCursor = -1;
_host.SendChatBoxFromExternal(tab, text);
}
// History-navigation callback for the compact input. Mirrors the main
// window's logic but operates on _state.HistoryCursor and the shared
// InputHistoryService. Index semantics match v0.5.x InputBacklog:
// 0 = oldest, Count-1 = newest.
private unsafe int CompactCallback(scoped ref ImGuiInputTextCallbackData data)
{
if (data.EventFlag != ImGuiInputTextFlags.CallbackHistory)
return 0;
var prev = _state.HistoryCursor;
switch (data.EventKey)
{
case ImGuiKey.UpArrow:
switch (_state.HistoryCursor)
{
case -1:
var offset = 0;
if (!string.IsNullOrWhiteSpace(_state.Buffer))
{
InputHistoryService.Push(_state.Buffer);
offset = 1;
}
_state.HistoryCursor = InputHistoryService.Count - 1 - offset;
break;
case > 0:
_state.HistoryCursor--;
break;
}
break;
case ImGuiKey.DownArrow:
if (_state.HistoryCursor != -1)
if (++_state.HistoryCursor >= InputHistoryService.Count)
_state.HistoryCursor = -1;
break;
}
if (prev == _state.HistoryCursor)
return 0;
var historyStr = InputHistoryService.GetByCursor(_state.HistoryCursor) ?? string.Empty;
data.DeleteChars(0, data.BufTextLen);
data.InsertChars(0, historyStr);
return 0;
}
private void DrawChannelIconButton(Tab tab)
{
var inputType = tab.CurrentChannel.UseTempChannel
? tab.CurrentChannel.TempChannel.ToChatType()
: tab.CurrentChannel.Channel.ToChatType();
var rgba = Plugin.Config.ChatColours.TryGetValue(inputType, out var c)
? c
: (inputType.DefaultColor() ?? 0xFFFFFFFFu);
var v3 = ColourUtil.RgbaToVector3(rgba);
var bg = new Vector4(v3.X, v3.Y, v3.Z, 1f);
// Compute readable foreground — black on bright, white on dark
var luminance = 0.2126f * v3.X + 0.7152f * v3.Y + 0.0722f * v3.Z;
var fg = luminance > 0.55f ? new Vector4(0f, 0f, 0f, 1f) : new Vector4(1f, 1f, 1f, 1f);
const string popupId = "chat-channel-picker-compact";
const float buttonSize = 22f;
using (ImRaii.PushColor(ImGuiCol.Button, bg))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, bg))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, bg))
using (ImRaii.PushColor(ImGuiCol.Text, fg))
{
// Single-letter glyph derived from the channel — quick visual cue
// until we have a proper icon font available in the compact bar.
var label = ChannelGlyph(inputType);
if (ImGui.Button($"{label}##chan-compact", new Vector2(buttonSize, buttonSize)) && tab.Channel is null)
ImGui.OpenPopup(popupId);
}
if (tab.Channel is not null && ImGui.IsItemHovered())
{
ImGui.SetTooltip(Resources.Language.ChatLog_SwitcherDisabled);
}
else if (ImGui.IsItemHovered())
{
ImGui.SetTooltip(inputType.Name());
}
using (var popup = ImRaii.Popup(popupId))
{
if (popup)
{
var channels = _host.GetValidChannels();
foreach (var (name, channel) in channels)
if (ImGui.Selectable(name))
_host.SetChannel(channel);
}
}
}
private static string ChannelGlyph(ChatType type) => type switch
{
ChatType.Say => "S",
ChatType.Yell => "Y",
ChatType.Shout => "!",
ChatType.TellIncoming or ChatType.TellOutgoing => "T",
ChatType.Party or ChatType.CrossParty => "P",
ChatType.Alliance => "A",
ChatType.FreeCompany => "F",
ChatType.NoviceNetwork => "N",
ChatType.Linkshell1 => "1",
ChatType.Linkshell2 => "2",
ChatType.Linkshell3 => "3",
ChatType.Linkshell4 => "4",
ChatType.Linkshell5 => "5",
ChatType.Linkshell6 => "6",
ChatType.Linkshell7 => "7",
ChatType.Linkshell8 => "8",
ChatType.CrossLinkshell1 => "①",
ChatType.CrossLinkshell2 => "②",
ChatType.CrossLinkshell3 => "③",
ChatType.CrossLinkshell4 => "④",
ChatType.CrossLinkshell5 => "⑤",
ChatType.CrossLinkshell6 => "⑥",
ChatType.CrossLinkshell7 => "⑦",
ChatType.CrossLinkshell8 => "⑧",
_ => "?",
};
// Forwards a tab-cycle keybind delta to the host so all windows
// navigate the same active-tab pointer (single source of truth).
public void HandleKeybindForward(int delta)
{
_host.ChangeTabDelta(delta);
}
}
// Per-window input state. Each ChatInputBar instance owns one of these
// so pop-outs and the main window keep independent buffers and channels
// (State-Sync-Entscheidung A in the v0.6.0 spec).
public sealed class InputState
{
public string Buffer = string.Empty;
public InputChannel? Channel;
public int HistoryCursor = -1;
}
@@ -3,11 +3,11 @@ using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.GameFunctions; using HellionChat.GameFunctions;
using ChatTwo.GameFunctions.Types; using HellionChat.GameFunctions.Types;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
@@ -22,7 +22,7 @@ using Dalamud.Bindings.ImGui;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Lumina.Extensions; using Lumina.Extensions;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
public sealed class ChatLogWindow : Window public sealed class ChatLogWindow : Window
{ {
@@ -44,7 +44,10 @@ public sealed class ChatLogWindow : Window
internal bool InputFocused { get; private set; } internal bool InputFocused { get; private set; }
private int ActivatePos = -1; private int ActivatePos = -1;
internal string Chat = string.Empty; internal string Chat = string.Empty;
private readonly List<string> InputBacklog = []; // Hellion Chat — v0.6.0 input history was extracted into
// InputHistoryService so pop-out windows with their own ChatInputBar
// share the same Up/Down history with the main window. The cursor
// stays window-local because each window navigates independently.
private int InputBacklogIdx = -1; private int InputBacklogIdx = -1;
public bool TellSpecial; public bool TellSpecial;
private readonly Stopwatch LastResize = new(); private readonly Stopwatch LastResize = new();
@@ -63,6 +66,14 @@ public sealed class ChatLogWindow : Window
public Vector2 LastWindowPos { get; private set; } = Vector2.Zero; public Vector2 LastWindowPos { get; private set; } = Vector2.Zero;
public Vector2 LastWindowSize { get; private set; } = Vector2.Zero; public Vector2 LastWindowSize { get; private set; } = Vector2.Zero;
// Window position recovery: guards against off-screen positions after a
// display layout change (monitor disconnected, resolution changed). On
// the first draw after plugin load we run a one-shot bounds check to see
// whether the stored position still overlaps any visible viewport area.
// The manual reset button in the settings forces the position regardless.
private bool DidOnLoadBoundsCheck;
internal bool RequestPositionReset { get; set; }
public unsafe ImGuiViewport* LastViewport; public unsafe ImGuiViewport* LastViewport;
private bool WasDocked; private bool WasDocked;
@@ -267,9 +278,11 @@ public sealed class ChatLogWindow : Window
{ {
case "hide": case "hide":
CurrentHideState = HideState.User; CurrentHideState = HideState.User;
Plugin.Log.Verbose("HideState: → User (chat hide command)");
break; break;
case "show": case "show":
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
Plugin.Log.Verbose("HideState: → None (chat show command)");
break; break;
case "toggle": case "toggle":
CurrentHideState = CurrentHideState switch CurrentHideState = CurrentHideState switch
@@ -279,6 +292,7 @@ public sealed class ChatLogWindow : Window
HideState.None => HideState.User, HideState.None => HideState.User,
_ => CurrentHideState, _ => CurrentHideState,
}; };
Plugin.Log.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
break; break;
} }
} }
@@ -330,16 +344,10 @@ public sealed class ChatLogWindow : Window
private void AddBacklog(string message) private void AddBacklog(string message)
{ {
for (var i = 0; i < InputBacklog.Count; i++) // v0.6.0 — delegates to the shared InputHistoryService so pop-out
{ // ChatInputBar instances see the same history. Move-to-newest
if (InputBacklog[i] != message) // deduplication lives inside the service.
continue; InputHistoryService.Push(message);
InputBacklog.RemoveAt(i);
break;
}
InputBacklog.Add(message);
} }
private float GetRemainingHeightForMessageLog() private float GetRemainingHeightForMessageLog()
@@ -350,6 +358,14 @@ public sealed class ChatLogWindow : Window
if (Plugin.Config.PreviewPosition is PreviewPosition.Inside) if (Plugin.Config.PreviewPosition is PreviewPosition.Inside)
height -= Plugin.InputPreview.PreviewHeight; height -= Plugin.InputPreview.PreviewHeight;
// Hellion Chat v0.6.1 — Header-Toolbar rendert auf Window-Ebene über
// einem horizontalen Layout-Pfad und wird von GetContentRegionAvail
// hier drin NICHT automatisch berücksichtigt, daher expliziter Abzug.
// Banner dagegen rendert in DrawChatLog VOR diesem ganzen Block und
// ImGui zieht seine Höhe automatisch von GetContentRegionAvail ab,
// weil der Cursor schon weiter unten steht — kein eigener Abzug.
height -= ImGui.GetFrameHeightWithSpacing();
return height; return height;
} }
@@ -393,30 +409,48 @@ public sealed class ChatLogWindow : Window
{ {
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it // if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it
if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle) if (Plugin.Config.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
{
CurrentHideState = HideState.Battle; CurrentHideState = HideState.Battle;
Plugin.Log.Verbose("HideState: None → Battle");
}
// If the chat is hidden because of battle, we reset it here // If the chat is hidden because of battle, we reset it here
if (CurrentHideState is HideState.Battle && !Plugin.InBattle) if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
{
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
Plugin.Log.Verbose("HideState: Battle → None");
}
// if the chat has no hide state and in a cutscene, set the hide state to cutscene // if the chat has no hide state and in a cutscene, set the hide state to cutscene
if (Plugin.Config.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive)) if (Plugin.Config.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
{ {
if (Plugin.Functions.Chat.CheckHideFlags()) if (Plugin.Functions.Chat.CheckHideFlags())
{
CurrentHideState = HideState.Cutscene; CurrentHideState = HideState.Cutscene;
Plugin.Log.Verbose("HideState: None → Cutscene");
}
} }
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none // 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) if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
{
Plugin.Log.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
}
// if the chat is hidden because of a cutscene and the chat has been activated, show chat // if the chat is hidden because of a cutscene and the chat has been activated, show chat
if (CurrentHideState == HideState.Cutscene && Activate) if (CurrentHideState == HideState.Cutscene && Activate)
{
CurrentHideState = HideState.CutsceneOverride; CurrentHideState = HideState.CutsceneOverride;
Plugin.Log.Verbose("HideState: Cutscene → CutsceneOverride (user activate)");
}
// if the user hid the chat and is now activating chat, reset the hide state // if the user hid the chat and is now activating chat, reset the hide state
if (CurrentHideState == HideState.User && Activate) if (CurrentHideState == HideState.User && Activate)
{
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
Plugin.Log.Verbose("HideState: User → None (activate)");
}
if (CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Plugin.Config.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn)) if (CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Plugin.Config.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn))
{ {
@@ -537,6 +571,22 @@ public sealed class ChatLogWindow : Window
LastWindowSize = currentSize; LastWindowSize = currentSize;
LastWindowPos = ImGui.GetWindowPos(); LastWindowPos = ImGui.GetWindowPos();
// Window position recovery. Manual reset takes precedence and snaps
// the window to the safe default unconditionally; the one-shot
// on-load check only fires when the persisted position has no
// overlap with any visible viewport area.
if (RequestPositionReset)
{
RequestPositionReset = false;
DidOnLoadBoundsCheck = true;
ApplySafeDefaultPosition("manual-reset");
}
else if (!DidOnLoadBoundsCheck)
{
DidOnLoadBoundsCheck = true;
EnsureWindowOnScreen("on-load");
}
if (resized) if (resized)
LastResize.Restart(); LastResize.Restart();
@@ -546,6 +596,12 @@ public sealed class ChatLogWindow : Window
if (IsChatMode && Plugin.InputPreview.IsDrawable) if (IsChatMode && Plugin.InputPreview.IsDrawable)
Plugin.InputPreview.CalculatePreview(); Plugin.InputPreview.CalculatePreview();
// Hellion Chat v0.6.1 — render the one-time hint banner first so it
// sits above the tab area / sidebar in full window width. Stash the
// height for GetRemainingHeightForMessageLog so the message log
// shrinks accordingly while the banner is visible.
_v061HintBannerHeight = DrawV061HintBannerIfNeeded();
if (Plugin.Config.SidebarTabView) if (Plugin.Config.SidebarTabView)
DrawTabSidebar(); DrawTabSidebar();
else else
@@ -565,9 +621,40 @@ public sealed class ChatLogWindow : Window
DrawChannelName(activeTab); DrawChannelName(activeTab);
} }
// v1.0.2 — compute inputColour up front so the channel selector button
// can also tint with it (existing input-text push remains below).
var inputType = activeTab.CurrentChannel.UseTempChannel ? activeTab.CurrentChannel.TempChannel.ToChatType() : activeTab.CurrentChannel.Channel.ToChatType();
var isCommand = Chat.Trim().StartsWith('/');
if (isCommand)
{
var command = Chat.Split(' ')[0];
if (TextCommandChannels.TryGetValue(command, out var channel))
inputType = channel;
if (!IsValidCommand(command))
inputType = ChatType.Error;
}
var inputColour = Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) ? inputCol : inputType.DefaultColor();
if (!isCommand && Plugin.ExtraChat.ChannelOverride is var (_, overrideColour))
inputColour = overrideColour;
if (isCommand && Plugin.ExtraChat.ChannelCommandColours.TryGetValue(Chat.Split(' ')[0], out var ecColour))
inputColour = ecColour;
var beforeIcon = ImGui.GetCursorPos(); var beforeIcon = ImGui.GetCursorPos();
if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment) && activeTab.Channel is null)
ImGui.OpenPopup(ChatChannelPicker); var tintSelector = Plugin.Config.ColorSelectedInputChannelButton && inputColour.HasValue;
var selectorAbgr = tintSelector ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0u;
using (ImRaii.PushColor(ImGuiCol.Button, selectorAbgr, tintSelector))
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, ColourUtil.AdjustBrightness(selectorAbgr, 1.15f), tintSelector))
using (ImRaii.PushColor(ImGuiCol.ButtonActive, ColourUtil.AdjustBrightness(selectorAbgr, 0.85f), tintSelector))
{
if (ImGuiUtil.IconButton(FontAwesomeIcon.Comment) && activeTab.Channel is null)
ImGui.OpenPopup(ChatChannelPicker);
}
if (activeTab.Channel is not null && ImGui.IsItemHovered()) if (activeTab.Channel is not null && ImGui.IsItemHovered())
ImGuiUtil.Tooltip(Language.ChatLog_SwitcherDisabled); ImGuiUtil.Tooltip(Language.ChatLog_SwitcherDisabled);
@@ -591,27 +678,7 @@ public sealed class ChatLogWindow : Window
var buttonsRight = (showNovice ? 1 : 0) + (Plugin.Config.ShowHideButton ? 1 : 0); var buttonsRight = (showNovice ? 1 : 0) + (Plugin.Config.ShowHideButton ? 1 : 0);
var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth * (1 + buttonsRight); var inputWidth = ImGui.GetContentRegionAvail().X - buttonWidth * (1 + buttonsRight);
var inputType = activeTab.CurrentChannel.UseTempChannel ? activeTab.CurrentChannel.TempChannel.ToChatType() : activeTab.CurrentChannel.Channel.ToChatType();
var isCommand = Chat.Trim().StartsWith('/');
if (isCommand)
{
var command = Chat.Split(' ')[0];
if (TextCommandChannels.TryGetValue(command, out var channel))
inputType = channel;
if (!IsValidCommand(command))
inputType = ChatType.Error;
}
var normalColor = ImGui.GetColorU32(ImGuiCol.Text); var normalColor = ImGui.GetColorU32(ImGuiCol.Text);
var inputColour = Plugin.Config.ChatColours.TryGetValue(inputType, out var inputCol) ? inputCol : inputType.DefaultColor();
if (!isCommand && Plugin.ExtraChat.ChannelOverride is var (_, overrideColour))
inputColour = overrideColour;
if (isCommand && Plugin.ExtraChat.ChannelCommandColours.TryGetValue(Chat.Split(' ')[0], out var ecColour))
inputColour = ecColour;
var push = inputColour != null; var push = inputColour != null;
using (ImRaii.PushColor(ImGuiCol.Text, push ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0, push)) using (ImRaii.PushColor(ImGuiCol.Text, push ? ColourUtil.RgbaToAbgr(inputColour!.Value) : 0, push))
{ {
@@ -925,6 +992,18 @@ public sealed class ChatLogWindow : Window
]; ];
} }
// v0.6.0 — pop-out windows route submission through this wrapper.
// The main-window Chat buffer is briefly used as a vehicle for
// SendChatBox (which reads it directly) and restored afterwards so
// the main window does not visibly lose any half-typed input.
internal void SendChatBoxFromExternal(Tab tab, string text)
{
var saved = Chat;
Chat = text;
SendChatBox(tab);
Chat = saved;
}
internal void SendChatBox(Tab activeTab) internal void SendChatBox(Tab activeTab)
{ {
if (!string.IsNullOrWhiteSpace(Chat)) if (!string.IsNullOrWhiteSpace(Chat))
@@ -1286,6 +1365,7 @@ public sealed class ChatLogWindow : Window
TabSwitched(tab, previousTab); TabSwitched(tab, previousTab);
tab.Unread = 0; tab.Unread = 0;
DrawChatHeaderToolbar(tab);
DrawMessageLog(tab, PayloadHandler, GetRemainingHeightForMessageLog(), hasTabSwitched); DrawMessageLog(tab, PayloadHandler, GetRemainingHeightForMessageLog(), hasTabSwitched);
} }
@@ -1418,11 +1498,85 @@ public sealed class ChatLogWindow : Window
} }
if (currentTab > -1) if (currentTab > -1)
{
DrawChatHeaderToolbar(Plugin.Config.Tabs[currentTab]);
DrawMessageLog(Plugin.Config.Tabs[currentTab], PayloadHandler, childHeight, hasTabSwitched); DrawMessageLog(Plugin.Config.Tabs[currentTab], PayloadHandler, childHeight, hasTabSwitched);
}
Plugin.WantedTab = null; Plugin.WantedTab = null;
} }
// Hellion Chat v0.6.1 — visible pop-out trigger right above the message
// log so users discover the feature without having to right-click the tab.
// Renders only for the active tab in the main ChatLogWindow; pop-out
// windows have their own render path and skip this toolbar.
private void DrawChatHeaderToolbar(Tab tab)
{
var avail = ImGui.GetContentRegionAvail().X;
var iconWidth = ImGui.GetFrameHeight();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + avail - iconWidth);
if (ImGuiUtil.IconButton(FontAwesomeIcon.WindowRestore, tooltip: Language.ChatLog_Tabs_PopOut))
{
tab.PopOut = true;
Plugin.SaveConfig();
}
}
// Hellion Chat v0.6.1 — One-Time-Hint-Banner introducing the chat header
// pop-out toolbar button and the right-click pathway. Reuses the visual
// pattern from Popout.cs DrawHintBannerIfNeeded so users see a familiar
// dismiss-affordance. Returns the vertical space the banner consumed
// (0 when not shown) so the message log can shrink accordingly.
private float DrawV061HintBannerIfNeeded()
{
if (Plugin.Config.SeenPopOutHeaderHint)
return 0f;
var hintText = Resources.HellionStrings.Hint_v061_PopOutHeader_Body;
var ackLabel = Resources.HellionStrings.Hint_v061_PopOutHeader_Ack;
var openLabel = Resources.HellionStrings.Hint_v061_PopOutHeader_OpenSettings;
var startY = ImGui.GetCursorPosY();
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
ImGui.PushStyleColor(ImGuiCol.ChildBg, bg);
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
var dismiss = false;
var openSettings = false;
using (var child = ImRaii.Child("##v061-pop-out-header-hint", new System.Numerics.Vector2(0f, 84f), true))
{
if (child)
{
ImGui.TextWrapped(hintText);
if (ImGui.Button(ackLabel))
dismiss = true;
ImGui.SameLine();
if (ImGui.Button(openLabel))
{
dismiss = true;
openSettings = true;
}
}
}
ImGui.PopStyleVar();
ImGui.PopStyleColor();
ImGui.Spacing();
if (dismiss)
{
Plugin.Config.SeenPopOutHeaderHint = true;
Plugin.SaveConfig();
Plugin.Log.Debug("v0.6.1 pop-out header hint dismissed");
if (openSettings)
Plugin.SettingsWindow.Toggle();
}
return ImGui.GetCursorPosY() - startY;
}
private void DrawTabContextMenu(Tab tab, int i) private void DrawTabContextMenu(Tab tab, int i)
{ {
using var contextMenu = ImRaii.ContextPopupItem($"tab-context-menu-{i}"); using var contextMenu = ImRaii.ContextPopupItem($"tab-context-menu-{i}");
@@ -1481,6 +1635,20 @@ public sealed class ChatLogWindow : Window
internal readonly List<bool> PopOutDocked = []; internal readonly List<bool> PopOutDocked = [];
internal readonly HashSet<Guid> PopOutWindows = []; internal readonly HashSet<Guid> PopOutWindows = [];
// Hellion Chat v0.6.1 — height the v0.6.1 hint banner consumed in the
// current frame, read by GetRemainingHeightForMessageLog so the message
// log can shrink. Unconditionally reassigned at the top of DrawChatLog
// (before any tab-area render) so the value is always in sync with the
// current frame. Returns 0 once the banner is dismissed.
private float _v061HintBannerHeight;
// v0.6.0 — live enumeration of all active Popout windows so the
// KeybindManager can find a focused ChatInputBar to forward tab-cycle
// keybinds to. Filter on IsOpen prevents touching closed-but-still-
// registered popouts.
internal IEnumerable<Popout> ActivePopouts =>
Plugin.WindowSystem.Windows.OfType<Popout>().Where(p => p.IsOpen);
private void AddPopOutsToDraw() private void AddPopOutsToDraw()
{ {
HandlerLender.ResetCounter(); HandlerLender.ResetCounter();
@@ -1757,7 +1925,7 @@ public sealed class ChatLogWindow : Window
offset = 1; offset = 1;
} }
InputBacklogIdx = InputBacklog.Count - 1 - offset; InputBacklogIdx = InputHistoryService.Count - 1 - offset;
break; break;
case > 0: case > 0:
InputBacklogIdx--; InputBacklogIdx--;
@@ -1766,7 +1934,7 @@ public sealed class ChatLogWindow : Window
break; break;
case ImGuiKey.DownArrow: case ImGuiKey.DownArrow:
if (InputBacklogIdx != -1) if (InputBacklogIdx != -1)
if (++InputBacklogIdx >= InputBacklog.Count) if (++InputBacklogIdx >= InputHistoryService.Count)
InputBacklogIdx = -1; InputBacklogIdx = -1;
break; break;
} }
@@ -1774,7 +1942,7 @@ public sealed class ChatLogWindow : Window
if (prevPos == InputBacklogIdx) if (prevPos == InputBacklogIdx)
return 0; return 0;
var historyStr = InputBacklogIdx >= 0 ? InputBacklog[InputBacklogIdx] : string.Empty; var historyStr = InputHistoryService.GetByCursor(InputBacklogIdx) ?? string.Empty;
data.DeleteChars(0, data.BufTextLen); data.DeleteChars(0, data.BufTextLen);
data.InsertChars(0, historyStr); data.InsertChars(0, historyStr);
@@ -1923,4 +2091,47 @@ public sealed class ChatLogWindow : Window
var hashCode = $"{Salt}{playerName}{worldId}".GetHashCode(); var hashCode = $"{Salt}{playerName}{worldId}".GetHashCode();
return $"Player {hashCode:X8}"; return $"Player {hashCode:X8}";
} }
// Snap threshold in pixels: at least this much of the window must overlap
// a visible viewport so the user can still grab the first tab header.
// Below the threshold the window is considered off-screen.
private const int OnScreenMinOverlapX = 100;
private const int OnScreenMinOverlapY = 40;
// Default snap position relative to the primary viewport (top-left with a
// safety margin from the game title bar).
private static readonly Vector2 SafeDefaultOffset = new(50, 50);
private void EnsureWindowOnScreen(string source)
{
if (LastWindowSize.X < 1 || LastWindowSize.Y < 1)
return;
var viewport = ImGui.GetMainViewport();
var visibleMin = viewport.WorkPos;
var visibleMax = viewport.WorkPos + viewport.WorkSize;
var overlapMin = Vector2.Max(LastWindowPos, visibleMin);
var overlapMax = Vector2.Min(LastWindowPos + LastWindowSize, visibleMax);
var overlap = overlapMax - overlapMin;
if (overlap.X >= OnScreenMinOverlapX && overlap.Y >= OnScreenMinOverlapY)
return;
ApplySafeDefaultPosition(source);
}
private void ApplySafeDefaultPosition(string source)
{
var viewport = ImGui.GetMainViewport();
var safePos = viewport.WorkPos + SafeDefaultOffset;
Position = safePos;
Plugin.Log.Info(
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}.");
// Pop-outs are intentionally non-persistent (cleared on plugin reload),
// so an off-screen pop-out can never survive a session boundary. The
// main window above is the only persistence target that needs an
// explicit recovery path.
}
} }
@@ -1,12 +1,12 @@
using System.Numerics; using System.Numerics;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
public class CommandHelpWindow : Window { public class CommandHelpWindow : Window {
private ChatLogWindow LogWindow { get; } private ChatLogWindow LogWindow { get; }
@@ -2,9 +2,9 @@
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Text; using System.Text;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
@@ -18,7 +18,7 @@ using Lumina.Data.Files;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using MoreLinq; using MoreLinq;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
public class DbViewer : Window public class DbViewer : Window
{ {
@@ -391,6 +391,10 @@ public class DbViewer : Window
await rangeMessageEnumerator.DisposeAsync(); await rangeMessageEnumerator.DisposeAsync();
var filteredHistory = Filter(messageHistory); var filteredHistory = Filter(messageHistory);
// Materialize Count once — re-enumerating the IEnumerable on
// every batch (twice per batch in the Notification update)
// turned the export into an O(N²) hot loop on large histories.
var totalCount = filteredHistory.Count;
var sb = new StringBuilder(); var sb = new StringBuilder();
await using var stream = new StreamWriter(Path.Join(InputPath, $"Chat2_{DateTime.Now:yyyy_dd_M__HH_mm_ss}.txt")); await using var stream = new StreamWriter(Path.Join(InputPath, $"Chat2_{DateTime.Now:yyyy_dd_M__HH_mm_ss}.txt"));
@@ -416,8 +420,8 @@ public class DbViewer : Window
} }
}, delayTicks: 5); }, delayTicks: 5);
Notification.Progress = (float)batch / filteredHistory.Count; Notification.Progress = (float)batch / totalCount;
Notification.Content = $"Exported {batch} of {filteredHistory.Count} messages"; Notification.Content = $"Exported {batch} of {totalCount} messages";
await stream.WriteAsync(sb.ToString()); await stream.WriteAsync(sb.ToString());
sb.Clear(); sb.Clear();
} }
@@ -1,5 +1,5 @@
using System.Numerics; using System.Numerics;
using ChatTwo.Code; using HellionChat.Code;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
@@ -7,9 +7,9 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
public class DebuggerWindow : Window public class DebuggerWindow : Window, IDisposable
{ {
private readonly Plugin Plugin; private readonly Plugin Plugin;
private readonly ChatLogWindow ChatLogWindow; private readonly ChatLogWindow ChatLogWindow;
@@ -1,13 +1,13 @@
using System.Numerics; using System.Numerics;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Privacy; using HellionChat.Privacy;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
public sealed class FirstRunWizard : Window public sealed class FirstRunWizard : Window
{ {
@@ -1,8 +1,8 @@
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
/// <summary> /// <summary>
/// ImGui style override for Hellion Chat. Industrial HUD palette with three /// ImGui style override for Hellion Chat. Industrial HUD palette with three
@@ -1,9 +1,9 @@
using System.Numerics; using System.Numerics;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
@@ -12,7 +12,7 @@ using Dalamud.Interface.Windowing;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
public partial class InputPreview : Window public partial class InputPreview : Window
{ {
@@ -4,7 +4,7 @@ using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
internal class Popout : Window internal class Popout : Window
{ {
@@ -15,6 +15,18 @@ internal class Popout : Window
private long FrameTime; // set every frame private long FrameTime; // set every frame
private long LastActivityTime = Environment.TickCount64; private long LastActivityTime = Environment.TickCount64;
// v0.6.0 — optional input bar inside the pop-out window. Lazy-allocated
// when the user enables Tab.PopOutInputEnabled and torn down when the
// toggle is turned off (independent text buffer is intentionally
// discarded — see v0.6.0 spec edge-case P1).
public ChatInputBar? InputBar { get; private set; }
public bool HasFocusedInputBar => InputBar?.IsFocused ?? false;
// Hellion Chat — v0.6.1 expose just the tab identifier (not the whole Tab
// reference) so AutoTellTabsService.DropOldestTempTab can locate the
// matching pop-out window when an LRU temp tab gets evicted.
internal Guid TabIdentifier => Tab.Identifier;
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx) : base($"{tab.Name}##popout") public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx) : base($"{tab.Name}##popout")
{ {
ChatLogWindow = chatLogWindow; ChatLogWindow = chatLogWindow;
@@ -68,7 +80,10 @@ internal class Popout : Window
if (!Tab.CanResize) if (!Tab.CanResize)
Flags |= ImGuiWindowFlags.NoResize; Flags |= ImGuiWindowFlags.NoResize;
if (!ChatLogWindow.PopOutDocked[Idx]) // Idx may point past the end if PopOutDocked was resized (e.g., a tab
// dropped) between the AddPopOutsToDraw() snapshot and this frame.
// Guard the read so we don't index into stale state.
if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count && !ChatLogWindow.PopOutDocked[Idx])
{ {
if (Tab.IndependentOpacity) if (Tab.IndependentOpacity)
{ {
@@ -93,16 +108,98 @@ internal class Popout : Window
ImGui.Separator(); ImGui.Separator();
} }
// v0.6.0 — one-time hint banner explaining the new pop-out input
// feature. Shown once per user; "Got it" or "Open settings"
// dismisses it and persists the flag.
var hintBannerHeight = DrawHintBannerIfNeeded();
// v0.6.0 — pop-out optional input bar. Reserve height first so the
// message log draws into the right region; only shown when the
// global master switch is on. Toggle-OFF resets InputBar so the
// next toggle-ON gives a fresh buffer (no stale text persists).
var inputEnabled = Plugin.Config.PopOutInputEnabled;
if (!inputEnabled && InputBar != null)
{
InputBar = null;
}
if (inputEnabled)
{
InputBar ??= new ChatInputBar(ChatLogWindow.Plugin, ChatLogWindow, () => Tab);
}
var inputBarHeight = inputEnabled
? ImGui.GetFrameHeightWithSpacing() + ImGui.GetStyle().ItemSpacing.Y
: 0f;
var handler = ChatLogWindow.HandlerLender.Borrow(); var handler = ChatLogWindow.HandlerLender.Borrow();
ChatLogWindow.DrawMessageLog(Tab, handler, ImGui.GetContentRegionAvail().Y, false); var logHeight = ImGui.GetContentRegionAvail().Y - inputBarHeight - hintBannerHeight;
ChatLogWindow.DrawMessageLog(Tab, handler, logHeight, false);
if (inputEnabled && InputBar != null)
{
ImGui.Separator();
InputBar.RenderCompact();
}
if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows)) if (ImGui.IsWindowHovered(ImGuiHoveredFlags.ChildWindows))
LastActivityTime = FrameTime; LastActivityTime = FrameTime;
} }
// Returns the vertical space the banner consumed (0 when not shown)
// so the message log can shrink accordingly.
private float DrawHintBannerIfNeeded()
{
if (Plugin.Config.SeenPopOutInputHint)
return 0f;
var hintText = Resources.HellionStrings.Popout_v060_HintText;
var ackLabel = Resources.HellionStrings.Popout_v060_HintAck;
var openLabel = Resources.HellionStrings.Popout_v060_HintOpenSettings;
var startY = ImGui.GetCursorPosY();
var bg = new System.Numerics.Vector4(0.16f, 0.20f, 0.28f, 1f);
ImGui.PushStyleColor(ImGuiCol.ChildBg, bg);
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1f);
var dismiss = false;
var openSettings = false;
using (var child = ImRaii.Child("##v060-pop-out-hint", new System.Numerics.Vector2(0f, 64f), true))
{
if (child)
{
ImGui.TextWrapped(hintText);
if (ImGui.Button(ackLabel))
dismiss = true;
ImGui.SameLine();
if (ImGui.Button(openLabel))
{
dismiss = true;
openSettings = true;
}
}
}
ImGui.PopStyleVar();
ImGui.PopStyleColor();
ImGui.Spacing();
if (dismiss)
{
Plugin.Config.SeenPopOutInputHint = true;
ChatLogWindow.Plugin.SaveConfig();
Plugin.Log.Debug("Pop-Out input hint dismissed");
if (openSettings)
ChatLogWindow.Plugin.SettingsWindow.Toggle();
}
return ImGui.GetCursorPosY() - startY;
}
public override void PostDraw() public override void PostDraw()
{ {
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked(); if (Idx >= 0 && Idx < ChatLogWindow.PopOutDocked.Count)
ChatLogWindow.PopOutDocked[Idx] = ImGui.IsWindowDocked();
if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null }) if (Plugin.Config is { OverrideStyle: true, ChosenStyle: not null })
StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop(); StyleModel.GetConfiguredStyles()?.FirstOrDefault(style => style.Name == Plugin.Config.ChosenStyle)?.Pop();
@@ -132,30 +229,48 @@ internal class Popout : Window
{ {
// if the chat has no hide state set, and the player has entered battle, we hide chat if they have configured it // 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) if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
{
CurrentHideState = HideState.Battle; CurrentHideState = HideState.Battle;
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Battle");
}
// If the chat is hidden because of battle, we reset it here // If the chat is hidden because of battle, we reset it here
if (CurrentHideState is HideState.Battle && !Plugin.InBattle) if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
{
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Battle → None");
}
// if the chat has no hide state and in a cutscene, set the hide state to cutscene // 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 (Tab.HideDuringCutscenes && CurrentHideState == HideState.None && (Plugin.CutsceneActive || Plugin.GposeActive))
{ {
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags()) if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
{
CurrentHideState = HideState.Cutscene; CurrentHideState = HideState.Cutscene;
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: None → Cutscene");
}
} }
// if the chat is hidden because of a cutscene and no longer in a cutscene, set the hide state to none // 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) if (CurrentHideState is HideState.Cutscene or HideState.CutsceneOverride && !Plugin.CutsceneActive && !Plugin.GposeActive)
{
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: {CurrentHideState} → None (cutscene/gpose ended)");
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
}
// if the chat is hidden because of a cutscene and the chat has been activated, show chat // if the chat is hidden because of a cutscene and the chat has been activated, show chat
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate) if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
{
CurrentHideState = HideState.CutsceneOverride; CurrentHideState = HideState.CutsceneOverride;
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: Cutscene → CutsceneOverride (user activate)");
}
// if the user hid the chat and is now activating chat, reset the hide state // if the user hid the chat and is now activating chat, reset the hide state
if (CurrentHideState == HideState.User && ChatLogWindow.Activate) if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
{
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
Plugin.Log.Verbose($"Popout HideState [{Tab.Name}]: User → None (activate)");
}
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn); return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle || (Tab.HideWhenNotLoggedIn && !Plugin.ClientState.IsLoggedIn);
} }
@@ -1,7 +1,7 @@
using System.Globalization; using System.Globalization;
using System.Numerics; using System.Numerics;
using System.Text; using System.Text;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
@@ -10,7 +10,7 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Utility; using Dalamud.Utility;
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload; using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
public class SeStringDebugger : Window public class SeStringDebugger : Window
{ {
@@ -1,13 +1,13 @@
using System.Numerics; using System.Numerics;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Ui.SettingsTabs; using HellionChat.Ui.SettingsTabs;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Utility; using Dalamud.Utility;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui; namespace HellionChat.Ui;
public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
{ {
@@ -1,6 +1,6 @@
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud; using Dalamud;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.FontIdentifier;
@@ -9,7 +9,7 @@ using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
internal sealed class Appearance : ISettingsTab internal sealed class Appearance : ISettingsTab
{ {
@@ -230,6 +230,16 @@ internal sealed class Appearance : ISettingsTab
using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false)) using (ImRaii.PushIndent(ImGui.GetStyle().IndentSpacing, false))
{ {
DrawColourPresetButtons();
ImGui.TextDisabled(HellionStrings.Settings_Appearance_Colours_PresetsHint);
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.Checkbox(Language.Options_ColorSelectedInputChannelButton_Name, ref Mutable.ColorSelectedInputChannelButton);
ImGuiUtil.HelpMarker(Language.Options_ColorSelectedInputChannelButton_Description);
ImGui.Spacing();
foreach (var (_, types) in ChatTypeExt.SortOrder) foreach (var (_, types) in ChatTypeExt.SortOrder)
{ {
foreach (var type in types) foreach (var type in types)
@@ -263,6 +273,63 @@ internal sealed class Appearance : ISettingsTab
} }
} }
// Hellion Chat — v0.6.0 preset-buttons row above the per-channel colour
// editors. Apply is immediate and overwrites every channel covered by
// the preset; channels not in the preset keep their current colour.
private void DrawColourPresetButtons()
{
var first = true;
foreach (var (_, preset) in ChatColourPresets.All)
{
if (!first)
{
ImGui.SameLine();
}
first = false;
if (preset.IsBrandPreset)
{
// Hellion-Brand visuell hervorheben — blau-violetter Frame-Akzent
var border = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(255, 128, 200));
var btn = ColourUtil.RgbaToVector3(ColourUtil.ComponentsToRgba(74, 42, 106));
ImGui.PushStyleColor(ImGuiCol.Border, new System.Numerics.Vector4(border.X, border.Y, border.Z, 1f));
ImGui.PushStyleColor(ImGuiCol.Button, new System.Numerics.Vector4(btn.X, btn.Y, btn.Z, 1f));
ImGui.PushStyleVar(ImGuiStyleVar.FrameBorderSize, 1.5f);
}
if (ImGui.Button(GetPresetLabel(preset)))
{
ApplyPreset(preset);
}
if (preset.IsBrandPreset)
{
ImGui.PopStyleVar();
ImGui.PopStyleColor(2);
}
}
}
// Localized label for a preset; falls back to DisplayName if the i18n
// key is missing (defensive — the resource manager returns the key
// string itself when a lookup fails).
private static string GetPresetLabel(ChatColourPreset preset)
{
var localized = HellionStrings.ResourceManager.GetString(preset.LocalizationKey, HellionStrings.Culture);
return string.IsNullOrEmpty(localized) ? preset.DisplayName : localized;
}
private void ApplyPreset(ChatColourPreset preset)
{
foreach (var (channel, colour) in preset.Colours)
{
Mutable.ChatColours[channel] = colour;
}
Plugin.SaveConfig();
GlobalParametersCache.Refresh();
Plugin.Log.Debug($"Applied chat colour preset: {preset.DisplayName}");
}
private void DrawTimestampsSection() private void DrawTimestampsSection()
{ {
using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Timestamps_Heading); using var tree = ImRaii.TreeNode(HellionStrings.Settings_Appearance_Timestamps_Heading);
@@ -1,13 +1,13 @@
using System.Numerics; using System.Numerics;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
// Chat-Tab — vier eigenständige Sektionen: Auto-Tell-Tabs, Behaviour, // Chat-Tab — vier eigenständige Sektionen: Auto-Tell-Tabs, Behaviour,
// Preview, Emotes. Der Emotes-Block ist 1:1 aus der Bestand-Datei // Preview, Emotes. Der Emotes-Block ist 1:1 aus der Bestand-Datei
@@ -73,6 +73,9 @@ internal sealed class Chat : ISettingsTab
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay); ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_Compact_Name, ref Mutable.AutoTellTabsCompactDisplay);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description); ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_Compact_Description);
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Name, ref Mutable.AutoTellTabsOpenAsPopout);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_OpenAsPopout_Description);
ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name, ref Mutable.AutoTellTabsShowGreetedToggle); ImGui.Checkbox(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Name, ref Mutable.AutoTellTabsShowGreetedToggle);
ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description); ImGuiUtil.HelpMarker(HellionStrings.ChatLog_AutoTellTabs_GreetedToggle_Description);
@@ -166,6 +169,13 @@ internal sealed class Chat : ISettingsTab
using (Plugin.FontManager.FontAwesome.Push()) using (Plugin.FontManager.FontAwesome.Push())
ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0)); ImGui.Button(FontAwesomeIcon.Plus.ToIconString(), new Vector2(buttonWidth, 0));
// Open the selector popup on left-click; SelectorPopup uses
// ImRaii.ContextPopupItem internally which only opens on right-
// click otherwise — without this OpenPopup the button looked
// active but the popup never appeared on a normal click.
if (ImGui.IsItemClicked())
ImGui.OpenPopup("WordAddPopup");
if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions)) if (SearchSelector.SelectorPopup("WordAddPopup", out var newWord, WordPopupOptions))
{ {
Mutable.BlockedEmotes.Add(newWord); Mutable.BlockedEmotes.Add(newWord);
@@ -1,7 +1,7 @@
using System.Diagnostics; using System.Diagnostics;
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
@@ -9,7 +9,7 @@ using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Game.Text; using Dalamud.Game.Text;
namespace ChatTwo.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
internal sealed class Database : ISettingsTab internal sealed class Database : ISettingsTab
{ {
@@ -1,10 +1,10 @@
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
internal sealed class General : ISettingsTab internal sealed class General : ISettingsTab
{ {
@@ -1,4 +1,4 @@
namespace ChatTwo.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
internal interface ISettingsTab internal interface ISettingsTab
{ {
@@ -1,12 +1,12 @@
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
// Information-Tab vereint die früheren About- und Changelog-Tabs in // Information-Tab vereint die früheren About- und Changelog-Tabs in
// drei kollabierbaren Sektionen. Der About-Inhalt ist 1:1 aus About.cs // drei kollabierbaren Sektionen. Der About-Inhalt ist 1:1 aus About.cs
@@ -1,15 +1,15 @@
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Export; using HellionChat.Export;
using ChatTwo.Privacy; using HellionChat.Privacy;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.ImGuiNotification; using Dalamud.Interface.ImGuiNotification;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
internal sealed class Privacy : ISettingsTab internal sealed class Privacy : ISettingsTab
{ {
@@ -455,11 +455,18 @@ internal sealed class Privacy : ISettingsTab
if (deleted > 0) if (deleted > 0)
{ {
Plugin.Framework.Run(() => // Bound the wait so a hung framework tick can't deadlock
// the background retention worker. Five seconds is well
// beyond a normal frame; if we time out we log and let
// the next FilterAllTabsAsync call recover the state.
if (!Plugin.Framework.Run(() =>
{
Plugin.MessageManager.ClearAllTabs();
Plugin.MessageManager.FilterAllTabsAsync();
}).Wait(TimeSpan.FromSeconds(5)))
{ {
Plugin.MessageManager.ClearAllTabs(); Plugin.Log.Warning("Retention sweep: framework refresh timed out after 5s.");
Plugin.MessageManager.FilterAllTabsAsync(); }
}).Wait();
} }
WrapperUtil.AddNotification(string.Format(HellionStrings.Retention_Success, deleted), NotificationType.Success); WrapperUtil.AddNotification(string.Format(HellionStrings.Retention_Success, deleted), NotificationType.Success);
@@ -615,11 +622,17 @@ internal sealed class Privacy : ISettingsTab
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed); var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
Plugin.Log.Information($"Privacy cleanup: deleted {deleted} messages"); Plugin.Log.Information($"Privacy cleanup: deleted {deleted} messages");
Plugin.Framework.Run(() => // Bound the wait so a hung framework tick can't deadlock
// the background cleanup worker. See the matching comment in
// the retention path above for rationale.
if (!Plugin.Framework.Run(() =>
{
Plugin.MessageManager.ClearAllTabs();
Plugin.MessageManager.FilterAllTabsAsync();
}).Wait(TimeSpan.FromSeconds(5)))
{ {
Plugin.MessageManager.ClearAllTabs(); Plugin.Log.Warning("Privacy cleanup: framework refresh timed out after 5s.");
Plugin.MessageManager.FilterAllTabsAsync(); }
}).Wait();
WrapperUtil.AddNotification(string.Format(HellionStrings.Cleanup_Success, deleted), NotificationType.Success); WrapperUtil.AddNotification(string.Format(HellionStrings.Cleanup_Success, deleted), NotificationType.Success);
} }
@@ -1,12 +1,12 @@
using ChatTwo.Code; using HellionChat.Code;
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.SubKinds;
namespace ChatTwo.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
internal sealed class Tabs : ISettingsTab internal sealed class Tabs : ISettingsTab
{ {
@@ -181,28 +181,39 @@ internal sealed class Tabs : ISettingsTab
ImGui.SameLine(); ImGui.SameLine();
var selectedWorld = worlds.FindIndex(world => world.RowId == tab.TellTarget.World); // Guard against an empty worlds list — can happen briefly
if (selectedWorld == -1) // when switching characters or if the datacenter sheet
selectedWorld = 0; // has not yet populated. Without the guard the indexed
// access into worlds[selectedWorld] would crash.
using (var combo = ImRaii.Combo("###player-world", worlds[selectedWorld].Name.ToString())) if (worlds.Count == 0)
{ {
if (combo.Success) ImGui.TextDisabled("(no worlds available)");
}
else
{
var selectedWorld = worlds.FindIndex(world => world.RowId == tab.TellTarget.World);
if (selectedWorld == -1)
selectedWorld = 0;
using (var combo = ImRaii.Combo("###player-world", worlds[selectedWorld].Name.ToString()))
{ {
var lastDc = worlds.First().DataCenter.RowId; if (combo.Success)
foreach (var (idx, world) in worlds.Index())
{ {
if (ImGui.Selectable(world.Name.ToString(), selectedWorld == idx)) var lastDc = worlds.First().DataCenter.RowId;
foreach (var (idx, world) in worlds.Index())
{ {
selectedWorld = idx; if (ImGui.Selectable(world.Name.ToString(), selectedWorld == idx))
tab.TellTarget.World = worlds[selectedWorld].RowId; {
selectedWorld = idx;
tab.TellTarget.World = worlds[selectedWorld].RowId;
}
if (lastDc == world.DataCenter.RowId)
continue;
lastDc = world.DataCenter.RowId;
ImGui.Separator();
} }
if (lastDc == world.DataCenter.RowId)
continue;
lastDc = world.DataCenter.RowId;
ImGui.Separator();
} }
} }
} }
@@ -1,9 +1,9 @@
using ChatTwo.Resources; using HellionChat.Resources;
using ChatTwo.Util; using HellionChat.Util;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
namespace ChatTwo.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
internal sealed class Window : ISettingsTab internal sealed class Window : ISettingsTab
{ {
@@ -56,6 +56,9 @@ internal sealed class Window : ISettingsTab
ImGui.Checkbox(Language.Options_HideInBattle_Name, ref Mutable.HideInBattle); ImGui.Checkbox(Language.Options_HideInBattle_Name, ref Mutable.HideInBattle);
ImGuiUtil.HelpMarker(Language.Options_HideInBattle_Description); ImGuiUtil.HelpMarker(Language.Options_HideInBattle_Description);
ImGui.Checkbox(Language.Options_HideInNewGamePlusMenu_Name, ref Mutable.HideInNewGamePlusMenu);
ImGuiUtil.HelpMarker(Language.Options_HideInNewGamePlusMenu_Description);
} }
} }
@@ -133,11 +136,24 @@ internal sealed class Window : ISettingsTab
ImGui.Checkbox(Language.Options_ShowPopOutTitleBar_Name, ref Mutable.ShowPopOutTitleBar); ImGui.Checkbox(Language.Options_ShowPopOutTitleBar_Name, ref Mutable.ShowPopOutTitleBar);
// v0.6.0 — global master switch for the pop-out input bar.
ImGui.Checkbox(HellionStrings.Settings_Window_PopOutInputEnabled_Name, ref Mutable.PopOutInputEnabled);
ImGuiUtil.HelpMarker(HellionStrings.Settings_Window_PopOutInputEnabled_Description);
ImGui.Checkbox(Language.Options_ShowHideButton_Name, ref Mutable.ShowHideButton); ImGui.Checkbox(Language.Options_ShowHideButton_Name, ref Mutable.ShowHideButton);
ImGuiUtil.HelpMarker(Language.Options_ShowHideButton_Description); ImGuiUtil.HelpMarker(Language.Options_ShowHideButton_Description);
ImGui.Checkbox(Language.Options_SidebarTabView_Name, ref Mutable.SidebarTabView); ImGui.Checkbox(Language.Options_SidebarTabView_Name, ref Mutable.SidebarTabView);
ImGuiUtil.HelpMarker(string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName)); ImGuiUtil.HelpMarker(string.Format(Language.Options_SidebarTabView_Description, Plugin.PluginName));
ImGui.Spacing();
// Manual escape hatch for off-screen windows. The plugin already
// runs an automatic bounds check once per session, but a button
// is the user-friendly fallback after a display layout change.
if (ImGui.Button(HellionStrings.Settings_Window_ResetPosition_Name))
Plugin.ChatLogWindow.RequestPositionReset = true;
ImGuiUtil.HelpMarker(HellionStrings.Settings_Window_ResetPosition_Description);
} }
} }

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