Merge branch 'feature/v1.5.0'

This commit is contained in:
2026-05-17 11:45:16 +02:00
37 changed files with 1552 additions and 426 deletions
+37
View File
@@ -0,0 +1,37 @@
---
subtitle: DI Foundation und Service-Refactor
versionsnatur: Architektur-Cycle
---
- **Architektur-Umbau ohne User-spürbare Verhaltens-Änderung:** der
Plugin-Bootstrap wechselt auf einen Generic-Host DI-Container
(`Microsoft.Extensions.Hosting` + `IServiceCollection`) nach dem
Lightless-Sync-Muster. 18 Service-Klassen wandern von einem
statischen `Plugin.LogProxy`-Locator auf typisierte
`ILogger<T>`-Constructor-Injection. `DalamudLogger` brückt
`Microsoft.Extensions.Logging` über auf Dalamuds `IPluginLog`
im xllog erscheinen jetzt Service-spezifische Spalten wie
`[ MessageManager]` und `[Honori...ervice]`.
- **Plugin.LogProxy bleibt für die acht Buckets erhalten,** die
Constructor-Injection nicht erreicht: Static-Helper (EmoteCache,
AutoTranslate, MemoryUtil, WrapperUtil), Dalamud-Reflektion
(Configuration), Data-Class mit Massen-Instanziierung (Message)
und Instanz-Klassen die nur aus Static-Methods loggen (FontManager,
eine GameFunctions-Stelle).
- **Performance bestätigt durch Cross-Plugin-Baseline:** HellionChat
First-Frame-HITCH 77 ms Median, Chat 2 v1.40.2 74 ms Median — kein
DI-Penalty gegenüber dem Upstream-Fork-Origin. Lightless und
XIVInstantMessenger liegen bei ~7 ms weil sie ihren FontAtlas-Build
deferren; das wird das v1.5.1-Item.
- **User-sichtbarer Bug-Fix nebenbei:** Slash-Command-Einfügen in das
Chat-Eingabefeld (Friend-List "/tell"-Action plus Plugin-Inserts
von Artisan, AllaganTools und ähnlichen) ersetzt jetzt den
vorhandenen Input, statt anzukonkatenieren. Cherry-Pick aus ChatTwo
upstream `ee7768ac` mit Namespace-Anpassung.
- **Foundation für die Plugin-Integrations-Wave:** v1.5.7-11
(Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM
Quick-DM) werden ab jetzt strukturell handhabbar — neue Services
sind ein `services.AddSingleton<T>` plus ein paar Factory-Lambda-
Zeilen, kein Plugin.cs-Anflanschen mehr.
- Migration v17 unverändert: kein Schema-Bump, kein
Config-Migrations-Aufwand.
+18 -12
View File
@@ -9,6 +9,7 @@ using HellionChat.Code;
using HellionChat.GameFunctions.Types; using HellionChat.GameFunctions.Types;
using HellionChat.Resources; using HellionChat.Resources;
using HellionChat.Util; using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat; namespace HellionChat;
@@ -19,6 +20,7 @@ internal sealed class AutoTellTabsService : IDisposable
private readonly Plugin _plugin; private readonly Plugin _plugin;
private readonly MessageManager _messageManager; private readonly MessageManager _messageManager;
private readonly MessageStore _store; private readonly MessageStore _store;
private readonly ILogger<AutoTellTabsService> _logger;
private readonly object _tempTabsLock = new(); private readonly object _tempTabsLock = new();
// Hard cap on pinned TempTabs so the sidebar doesn't inflate over years // Hard cap on pinned TempTabs so the sidebar doesn't inflate over years
@@ -29,11 +31,17 @@ internal sealed class AutoTellTabsService : IDisposable
private bool _initialized; private bool _initialized;
internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store) internal AutoTellTabsService(
Plugin plugin,
MessageManager messageManager,
MessageStore store,
ILogger<AutoTellTabsService> logger
)
{ {
_plugin = plugin; _plugin = plugin;
_messageManager = messageManager; _messageManager = messageManager;
_store = store; _store = store;
_logger = logger;
} }
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply // Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
@@ -67,7 +75,7 @@ internal sealed class AutoTellTabsService : IDisposable
private void RehydratePinnedTabs() private void RehydratePinnedTabs()
{ {
var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool); var pinned = Plugin.Config.Tabs.Count(TabLifecycleHelpers.IsInPinnedPool);
Plugin.LogProxy.Debug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found"); _logger.LogDebug($"[Pin] Rehydrate scan: {pinned} pinned tab(s) found");
foreach (var tab in Plugin.Config.Tabs) foreach (var tab in Plugin.Config.Tabs)
{ {
@@ -76,7 +84,7 @@ internal sealed class AutoTellTabsService : IDisposable
if (tab.TellTarget is null || !tab.TellTarget.IsSet()) if (tab.TellTarget is null || !tab.TellTarget.IsSet())
{ {
Plugin.LogProxy.Warning( _logger.LogWarning(
$"[Pin] Pinned tab '{tab.Name}' has no usable TellTarget " $"[Pin] Pinned tab '{tab.Name}' has no usable TellTarget "
+ $"(Name={tab.TellTarget?.Name ?? "<null>"} World={tab.TellTarget?.World ?? 0}). " + $"(Name={tab.TellTarget?.Name ?? "<null>"} World={tab.TellTarget?.World ?? 0}). "
+ "Chat input on this tab will be empty until the partner sends a tell or you /tell manually." + "Chat input on this tab will be empty until the partner sends a tell or you /tell manually."
@@ -93,7 +101,7 @@ internal sealed class AutoTellTabsService : IDisposable
// sees the recent conversation, not a blank tab. // sees the recent conversation, not a blank tab.
PreloadHistory(tab, tab.TellTarget.Name, tab.TellTarget.World, Guid.Empty); PreloadHistory(tab, tab.TellTarget.Name, tab.TellTarget.World, Guid.Empty);
Plugin.LogProxy.Debug( _logger.LogDebug(
$"[Pin] Rehydrated '{tab.Name}' -> Tell target {tab.TellTarget.Name}@{tab.TellTarget.World}" $"[Pin] Rehydrated '{tab.Name}' -> Tell target {tab.TellTarget.Name}@{tab.TellTarget.World}"
); );
} }
@@ -130,7 +138,7 @@ internal sealed class AutoTellTabsService : IDisposable
if (partner == null) if (partner == null)
{ {
// Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases) // Diagnostics: helps detect regressions (FFXIV payload changes, new edge cases)
Plugin.LogProxy.Warning( _logger.LogWarning(
$"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, " $"[AutoTellTabs] Could not extract tell partner. type={message.Code.Type}, "
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, " + $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, " + $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
@@ -361,7 +369,7 @@ internal sealed class AutoTellTabsService : IDisposable
catch (Exception ex) catch (Exception ex)
{ {
// Non-fatal: tab still spawns with visible error notice instead of silent history loss // Non-fatal: tab still spawns with visible error notice instead of silent history loss
Plugin.LogProxy.Error(ex, "[AutoTellTabs] History preload failed"); _logger.LogError(ex, "[AutoTellTabs] History preload failed");
tab.Messages.AddPrune( tab.Messages.AddPrune(
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError), MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
MessageManager.MessageDisplayLimit MessageManager.MessageDisplayLimit
@@ -456,7 +464,7 @@ internal sealed class AutoTellTabsService : IDisposable
{ {
if (!tab.IsTempTab || tab.IsPinned) if (!tab.IsTempTab || tab.IsPinned)
{ {
Plugin.LogProxy.Debug( _logger.LogDebug(
$"[Pin] TryPin skipped: IsTempTab={tab.IsTempTab} IsPinned={tab.IsPinned}" $"[Pin] TryPin skipped: IsTempTab={tab.IsTempTab} IsPinned={tab.IsPinned}"
); );
return false; return false;
@@ -472,7 +480,7 @@ internal sealed class AutoTellTabsService : IDisposable
} }
tab.IsPinned = true; tab.IsPinned = true;
Plugin.LogProxy.Debug( _logger.LogDebug(
$"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}" $"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}"
); );
_plugin.SaveConfig(); _plugin.SaveConfig();
@@ -495,7 +503,7 @@ internal sealed class AutoTellTabsService : IDisposable
} }
tab.IsPinned = false; tab.IsPinned = false;
Plugin.LogProxy.Debug("[Pin] Unpinned tab '{tab.Name}'"); _logger.LogDebug("[Pin] Unpinned tab '{TabName}'", tab.Name);
_plugin.SaveConfig(); _plugin.SaveConfig();
} }
@@ -509,9 +517,7 @@ internal sealed class AutoTellTabsService : IDisposable
tab.IsTempTab = false; tab.IsTempTab = false;
tab.IsPinned = false; tab.IsPinned = false;
tab.TellTarget = TellTarget.Empty(); tab.TellTarget = TellTarget.Empty();
Plugin.LogProxy.Debug( _logger.LogDebug($"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)");
$"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)"
);
_plugin.SaveConfig(); _plugin.SaveConfig();
} }
} }
+9 -2
View File
@@ -1,10 +1,17 @@
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Microsoft.Extensions.Logging;
namespace HellionChat; namespace HellionChat;
internal sealed class Commands : IDisposable internal sealed class Commands : IDisposable
{ {
private readonly Dictionary<string, CommandWrapper> Registered = []; private readonly Dictionary<string, CommandWrapper> Registered = [];
private readonly ILogger<Commands> _logger;
public Commands(ILogger<Commands> logger)
{
_logger = logger;
}
public void Dispose() public void Dispose()
{ {
@@ -52,7 +59,7 @@ internal sealed class Commands : IDisposable
{ {
if (!Registered.TryGetValue(command, out var wrapper)) if (!Registered.TryGetValue(command, out var wrapper))
{ {
Plugin.LogProxy.Warning($"Missing registration for command {command}"); _logger.LogWarning($"Missing registration for command {command}");
return; return;
} }
@@ -62,7 +69,7 @@ internal sealed class Commands : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, $"Error while executing command {command}"); _logger.LogError(ex, $"Error while executing command {command}");
} }
} }
} }
+6 -3
View File
@@ -8,6 +8,9 @@ using Dalamud.Interface.Utility;
namespace HellionChat; namespace HellionChat;
// Two LogProxy sites live in static methods (TryGetHellionFontBytes,
// AddFontWithFallback); a ctor-injected ILogger would not be reachable
// from those scopes, so the class stays on Plugin.LogProxy.
public class FontManager public class FontManager
{ {
internal IFontHandle Axis = null!; internal IFontHandle Axis = null!;
@@ -234,9 +237,9 @@ public class FontManager
or ArgumentException or ArgumentException
) )
{ {
// Atlas-toolkit throws span IO and validation failures; routing the // Atlas-toolkit throws span IO and validation failures; routing
// wider set through the fallback keeps a corrupt font config from // the wider set through the fallback keeps a corrupt font config
// taking down the whole atlas build. // from taking down the whole atlas build.
Plugin.LogProxy.Warning( Plugin.LogProxy.Warning(
e, e,
$"Configured {slot} font failed to load ({e.GetType().Name}), " $"Configured {slot} font failed to load ({e.GetType().Name}), "
+11 -7
View File
@@ -19,6 +19,7 @@ using HellionChat.Resources;
using HellionChat.Util; using HellionChat.Util;
using InteropGenerator.Runtime; using InteropGenerator.Runtime;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using Microsoft.Extensions.Logging;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
namespace HellionChat.GameFunctions; namespace HellionChat.GameFunctions;
@@ -98,9 +99,12 @@ internal sealed unsafe class Chat : IDisposable
private long LastPlayerNameDisplayTypeRefresh; private long LastPlayerNameDisplayTypeRefresh;
private PlayerNameDisplayType CurrentPlayerNameDisplayType = PlayerNameDisplayType.FullName; private PlayerNameDisplayType CurrentPlayerNameDisplayType = PlayerNameDisplayType.FullName;
public Chat(Plugin plugin) private readonly ILogger<Chat> _logger;
public Chat(Plugin plugin, ILogger<Chat> logger)
{ {
Plugin = plugin; Plugin = plugin;
_logger = logger;
Plugin.GameInteropProvider.InitializeFromAttributes(this); Plugin.GameInteropProvider.InitializeFromAttributes(this);
ChatLogRefreshHook?.Enable(); ChatLogRefreshHook?.Enable();
@@ -236,7 +240,7 @@ internal sealed unsafe class Chat : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error in chat Activated event"); _logger.LogError(ex, "Error in chat Activated event");
} }
}); });
} }
@@ -266,7 +270,7 @@ internal sealed unsafe class Chat : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error in chat Activated event"); _logger.LogError(ex, "Error in chat Activated event");
} }
return 1; // Prevent vanilla chat log from gaining focus return 1; // Prevent vanilla chat log from gaining focus
@@ -299,7 +303,7 @@ internal sealed unsafe class Chat : IDisposable
{ {
playerName = SeString.Parse(agent->TellPlayerName).TextValue; playerName = SeString.Parse(agent->TellPlayerName).TextValue;
worldId = agent->TellWorldId; worldId = agent->TellWorldId;
Plugin.LogProxy.Debug($"Detected tell target '[redacted]'@{worldId}"); _logger.LogDebug($"Detected tell target '[redacted]'@{worldId}");
} }
Plugin.CurrentTab.CurrentChannel = new UsedChannel Plugin.CurrentTab.CurrentChannel = new UsedChannel
@@ -358,7 +362,7 @@ internal sealed unsafe class Chat : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error in chat Activated event"); _logger.LogError(ex, "Error in chat Activated event");
} }
} }
@@ -408,7 +412,7 @@ internal sealed unsafe class Chat : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error in chat Activated event"); _logger.LogError(ex, "Error in chat Activated event");
} }
} }
@@ -624,7 +628,7 @@ internal sealed unsafe class Chat : IDisposable
if (contentId == 0) if (contentId == 0)
{ {
Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error); Plugin.ChatGui.PrintError(Language.Chat_SendTell_Error);
Plugin.LogProxy.Warning( _logger.LogWarning(
"Tried to send a tell with ContentId being 0, sorry this is an internal error." "Tried to send a tell with ContentId being 0, sorry this is an internal error."
); );
return; return;
+12 -4
View File
@@ -14,6 +14,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Info;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Excel; using Lumina.Excel;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
namespace HellionChat.GameFunctions; namespace HellionChat.GameFunctions;
@@ -37,14 +38,20 @@ internal unsafe class GameFunctions : IDisposable
#endregion #endregion
private Plugin Plugin { get; } private Plugin Plugin { get; }
private readonly ILogger<GameFunctions> _logger;
internal KeybindManager KeybindManager { get; } internal KeybindManager KeybindManager { get; }
internal Chat Chat { get; } internal Chat Chat { get; }
internal GameFunctions(Plugin plugin) internal GameFunctions(
Plugin plugin,
ILogger<GameFunctions> logger,
ILoggerFactory loggerFactory
)
{ {
Plugin = plugin; Plugin = plugin;
KeybindManager = new KeybindManager(plugin); _logger = logger;
Chat = new Chat(Plugin); KeybindManager = new KeybindManager(plugin, loggerFactory.CreateLogger<KeybindManager>());
Chat = new Chat(Plugin, loggerFactory.CreateLogger<Chat>());
Plugin.GameInteropProvider.InitializeFromAttributes(this); Plugin.GameInteropProvider.InitializeFromAttributes(this);
ResolveTextCommandPlaceholderHook?.Enable(); ResolveTextCommandPlaceholderHook?.Enable();
@@ -215,6 +222,7 @@ internal unsafe class GameFunctions : IDisposable
} }
catch (Exception e) catch (Exception e)
{ {
// Static method, no instance _logger reachable here.
Plugin.LogProxy.Warning(e, "Unable to open adventurer plate"); Plugin.LogProxy.Warning(e, "Unable to open adventurer plate");
return false; return false;
} }
@@ -255,7 +263,7 @@ internal unsafe class GameFunctions : IDisposable
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName); var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
if (byteCount >= PlaceholderBufferSize) if (byteCount >= PlaceholderBufferSize)
{ {
Plugin.LogProxy.Warning( _logger.LogWarning(
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original." $"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
); );
ReplacementName = null; ReplacementName = null;
+6 -2
View File
@@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.UI;
using HellionChat.Code; using HellionChat.Code;
using HellionChat.GameFunctions.Types; using HellionChat.GameFunctions.Types;
using HellionChat.Util; using HellionChat.Util;
using Microsoft.Extensions.Logging;
using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag; using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag;
namespace HellionChat.GameFunctions; namespace HellionChat.GameFunctions;
@@ -306,9 +307,12 @@ internal unsafe class KeybindManager : IDisposable
// VirtualKey.OEM_CLEAR, // VirtualKey.OEM_CLEAR,
}; };
internal KeybindManager(Plugin plugin) private readonly ILogger<KeybindManager> _logger;
internal KeybindManager(Plugin plugin, ILogger<KeybindManager> logger)
{ {
Plugin = plugin; Plugin = plugin;
_logger = logger;
Plugin.GameInteropProvider.InitializeFromAttributes(this); Plugin.GameInteropProvider.InitializeFromAttributes(this);
// Handle keybinds from the game on every tick. // Handle keybinds from the game on every tick.
@@ -507,7 +511,7 @@ internal unsafe class KeybindManager : IDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error in chat Activated event"); _logger.LogError(ex, "Error in chat Activated event");
} }
} }
+9 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Dalamud.NET.Sdk/15.0.0"> <Project Sdk="Dalamud.NET.Sdk/15.0.0">
<PropertyGroup> <PropertyGroup>
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base --> <!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
<Version>1.4.10</Version> <Version>1.5.0</Version>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<!-- Use lock file to pin exact versions --> <!-- Use lock file to pin exact versions -->
@@ -15,6 +15,14 @@
<!-- Closed ranges prevent surprise major bumps during lock file regeneration --> <!-- Closed ranges prevent surprise major bumps during lock file regeneration -->
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" /> <PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.7" />
<!-- v1.5.0 DI-container foundation; matches Lightless pin (Hosting 10.0.7) -->
<PackageReference
Include="Microsoft.Extensions.DependencyInjection"
Version="[10.0.7, 11.0.0)"
/>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="[10.0.7, 11.0.0)" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="[10.0.7, 11.0.0)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="[10.0.7, 11.0.0)" />
<!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) --> <!-- SQLitePCLRaw override for CVE-2025-6965, CVE-2025-7709 (SQLite >= 3.50.3) -->
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" /> <PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
<PackageReference Include="morelinq" Version="4.4.0" /> <PackageReference Include="morelinq" Version="4.4.0" />
+48 -43
View File
@@ -35,6 +35,54 @@ tags:
- Replacement - Replacement
- Privacy - Privacy
changelog: |- changelog: |-
**v1.5.0 — DI Foundation and Service Refactor (2026-05-17)**
Major architecture cycle. The plugin bootstrap moves to a
generic-host DI container (Microsoft.Extensions.Hosting +
IServiceCollection) modelled on Lightless Sync. Service logging
moves from a static Plugin.LogProxy locator to typed
Microsoft.Extensions.Logging.ILogger<T> via constructor injection,
bridged over Dalamud's IPluginLog by a custom DalamudLogger trio.
What changes under the hood:
- 18 instance-class services migrate to ILogger<T> via constructor
injection across four slices: data layer (MessageStore,
MessageManager, AutoTellTabsService), IPC and integrations
(HonorificService, IpcManager, TypingIpc, ExtraChat, the three
GameFunctions classes), UI window layer (ChatLogWindow,
DbViewer, Popout, three settings tabs), and root (Commands,
ThemeRegistry, PayloadHandler).
- Plugin.LogProxy stays in place for the eight buckets ctor
injection cannot reach: static helpers (EmoteCache,
AutoTranslate, MemoryUtil, WrapperUtil), Dalamud-reflected
types (Configuration), the Message data class, and instance
classes that only log from static methods (FontManager, one
GameFunctions site).
- Plugin.cs finishes at 1012 lines — virtually identical to the
pre-cycle 1013. The new Phase-1 host build and Plugin.X bridge
wiring trade out exactly the service and window allocations
that previously lived in LoadAsync.
- Cross-plugin baseline confirms no performance penalty against
Chat 2: HellionChat first-frame HITCH 77 ms median, Chat 2
74 ms median. Lightless and XIVInstantMessenger sit around
7 ms by deferring their font-atlas build past Finished
loading — that pattern is the v1.5.1 follow-up.
User-visible:
- Slash-command insert fix: pasting a slash command into the
chat input (Friend List "/tell" action, plugin-driven inserts
from Artisan, AllaganTools etc.) now replaces the existing
input instead of concatenating. Cherry-picked from ChatTwo
upstream ee7768ac with namespace adaptation.
Migration v17 stays (no schema bump).
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
**v1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16)** **v1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16)**
Eleventh and final sub-patch of the v1.4.x polish-sweep series. Eleventh and final sub-patch of the v1.4.x polish-sweep series.
@@ -151,47 +199,4 @@ changelog: |-
--- ---
**v1.4.7 — Backlog Cleanup and Mid-Features (2026-05-13)**
Eighth sub-patch of the v1.4.x polish-sweep series. First
user-visible feature bundle since v1.4.5 — pinned tell tabs that
survive relog, opt-in Honorific glow rendering, and a configurable
sidebar.
- TempTell Pin: right-click a TempTell tab in the sidebar to pin
it. Pinned tabs survive relog, keep their conversation history
(loaded on demand from the message store), and stay bound to
the same /tell partner. Hard cap of 5 pinned tabs in a pool
separate from the 15-tab auto-tell pool — total ceiling is 20
tabs. New 'Pinned' section in the sidebar with its own divider
header
- Honorific Glow outline now renders when the title carries a
Glow colour. Opt-in via Settings → Integrations → 'Render glow
outlines (Honorific)' (default off, dodges the per-frame
DrawList overhead on low-end hardware). Gradient (Color3 /
GradientColourSet / Wave / Pulse) is parsed but rendered
statically — a later cycle will port the full animation
- Sidebar width is now configurable in Theme & Layout (range
44160 px). Default stays icon-only; widen to fit section
headers like 'Active Tells (3)' without truncation
- Settings Save no longer pops the chat input back to /tell with
a pinned partner — Configuration.UpdateFrom now preserves the
runtime CurrentChannel across the persistent-tab merge, and
TabSwitched deep-clones the seeded channel instead of sharing
the previous tab's UsedChannel
- Util/ImGuiUtil.cs DrawArrows IconButton id now uses
(id + 1).ToString() instead of the operator-precedence quirk
id + 1.ToString() — generated IDs stay numerically stable
- Internal: IPluginLogProxy indirection over Dalamud's IPluginLog
routes all ~91 Plugin.Log call sites through a testable proxy.
MessageStore.Migrate0 can now run in xUnit without loading
Dalamud.dll, closing the gap F12.1 left in v1.4.6
- Internal: TempTab counter switched from an Interlocked cached
field to a derived Tabs.Count(predicate) — pin-state transitions
are cold-path and don't need lock-free reads
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases Full history: https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases
@@ -0,0 +1,98 @@
using Dalamud.Plugin;
using HellionChat.Ipc;
using HellionChat.Themes;
using Microsoft.Extensions.Hosting;
namespace HellionChat.Infrastructure.Hosting;
// Adapter shells around IHostedService so the host triggers each service's
// existing init method without touching the service class itself. Empty
// adapters still earn their place: registering them forces an eager resolve
// at Build, which runs the service ctor (IPC subscribe etc.) right then
// instead of lazily on first GetRequiredService.
internal sealed class FontManagerInitHostedService(FontManager fontManager) : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
fontManager.BuildFonts();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class ThemeRegistryInitHostedService(ThemeRegistry registry) : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
// Materialise the lazy AllCustom enumerable so the slug lookup hits a
// warm cache; otherwise the first Switch falls through to the built-in
// default when Config.Theme points at a custom slug.
foreach (var _ in registry.AllCustom()) { }
registry.Switch(Plugin.Config.Theme);
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
// IPC subscribers do their wiring in the ctor, so StartAsync stays empty —
// the registration alone forces an eager resolve which runs that wiring.
internal sealed class IpcManagerInitHostedService(IpcManager ipc) : IHostedService
{
private readonly IpcManager _ipc = ipc;
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class TypingIpcInitHostedService(TypingIpc typingIpc) : IHostedService
{
private readonly TypingIpc _typingIpc = typingIpc;
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class ExtraChatInitHostedService(ExtraChat extraChat) : IHostedService
{
private readonly ExtraChat _extraChat = extraChat;
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class MessageManagerInitHostedService(
IDalamudPluginInterface pluginInterface,
MessageManager manager
) : IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
// FilterAllTabsAsync rebuilds the per-tab view from the message store;
// on Boot, tabs come up empty and the first chat events fill them, so
// we skip the rebuild to avoid a pointless full-history scan.
if (pluginInterface.Reason is not PluginLoadReason.Boot)
manager.FilterAllTabsAsync();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
internal sealed class AutoTellTabsServiceInitHostedService(AutoTellTabsService service)
: IHostedService
{
public Task StartAsync(CancellationToken cancellationToken)
{
service.Initialize();
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
@@ -0,0 +1,64 @@
using System.Text;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
namespace HellionChat.Infrastructure.Logging;
internal sealed class DalamudLogger : ILogger
{
private readonly string _name;
private readonly IPluginLog _pluginLog;
public DalamudLogger(string name, IPluginLog pluginLog)
{
_name = name;
_pluginLog = pluginLog;
}
IDisposable? ILogger.BeginScope<TState>(TState state) => default!;
// Filtering happens in Dalamud's /xllog. Letting every level through keeps
// the HellionChat side stateless; if we ever want a per-plugin floor we add
// a Config.LogLevel and tighten this method.
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter
)
{
if (!IsEnabled(logLevel))
return;
// U+200B between the bracket and the level is a quiet provenance
// marker; byte-distinguishable from any 1:1 port of this format.
if ((int)logLevel <= (int)LogLevel.Information)
{
_pluginLog.Information($"[{_name}]{{{(int)logLevel}}} {state}");
return;
}
var sb = new StringBuilder();
sb.Append($"[{_name}]{{{(int)logLevel}}} {state} {exception?.Message}");
if (!string.IsNullOrWhiteSpace(exception?.StackTrace))
sb.AppendLine(exception.StackTrace);
var inner = exception?.InnerException;
while (inner != null)
{
sb.AppendLine($"InnerException {inner}: {inner.Message}");
sb.AppendLine(inner.StackTrace);
inner = inner.InnerException;
}
if (logLevel == LogLevel.Warning)
_pluginLog.Warning(sb.ToString());
else if (logLevel == LogLevel.Error)
_pluginLog.Error(sb.ToString());
else
_pluginLog.Fatal(sb.ToString());
}
}
@@ -0,0 +1,73 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
namespace HellionChat.Infrastructure.Logging;
[ProviderAlias("Dalamud")]
public sealed class DalamudLoggingProvider : ILoggerProvider
{
// Hellion Forge Bronze (#C2410C). Mixed into the bootstrap fingerprint.
private const string HellionMarker = "HellionForgeBronzeC2410C";
private readonly ConcurrentDictionary<string, DalamudLogger> _loggers = new(
StringComparer.OrdinalIgnoreCase
);
private readonly IPluginLog _pluginLog;
public DalamudLoggingProvider(IPluginLog pluginLog)
{
_pluginLog = pluginLog;
EmitBootstrapBanner();
}
// One-shot per plugin load. Intentionally visible in xllog so uncredited
// ports of the DalamudLogger trio keep announcing their origin.
private void EmitBootstrapBanner()
{
var version =
typeof(DalamudLoggingProvider).Assembly.GetName().Version?.ToString() ?? "0.0.0";
var fingerprint = ComputeFingerprint(version);
_pluginLog.Information(
$"HellionChat DI-Logger bootstrap v{version} fingerprint={fingerprint}"
);
}
private static string ComputeFingerprint(string version)
{
var seed = Encoding.UTF8.GetBytes($"{HellionMarker}-{version}");
var hash = SHA256.HashData(seed);
var sb = new StringBuilder(8);
for (var i = 0; i < 4; i++)
sb.Append(hash[i].ToString("x2"));
return sb.ToString();
}
public ILogger CreateLogger(string categoryName)
{
// Category-name normalisation mirrors Lightless: take the leaf type
// name, then either ellipsis-trim long ones or left-pad short ones to
// 15 chars so the xllog column stays aligned across services.
var catName = categoryName.Split(".", StringSplitOptions.RemoveEmptyEntries).Last();
if (catName.Length > 15)
catName = string.Concat(
catName.AsSpan(0, 6),
"...",
catName.AsSpan(catName.Length - 6, 6)
);
else
catName = catName.PadLeft(15);
return _loggers.GetOrAdd(catName, name => new DalamudLogger(name, _pluginLog));
}
public void Dispose()
{
_loggers.Clear();
GC.SuppressFinalize(this);
}
}
@@ -0,0 +1,23 @@
using Dalamud.Plugin.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
namespace HellionChat.Infrastructure.Logging;
public static class DalamudLoggingProviderExtensions
{
public static ILoggingBuilder AddDalamudLogging(
this ILoggingBuilder builder,
IPluginLog pluginLog
)
{
builder.ClearProviders();
builder.Services.TryAddEnumerable(
ServiceDescriptor.Singleton<ILoggerProvider, DalamudLoggingProvider>(
_ => new DalamudLoggingProvider(pluginLog)
)
);
return builder;
}
}
+7 -6
View File
@@ -2,6 +2,7 @@ using System;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace HellionChat.Integrations; namespace HellionChat.Integrations;
@@ -23,7 +24,7 @@ internal sealed class HonorificService : IDisposable
private readonly ICallGateSubscriber<object> _ready; private readonly ICallGateSubscriber<object> _ready;
private readonly ICallGateSubscriber<object> _disposing; private readonly ICallGateSubscriber<object> _disposing;
private readonly IPluginLog _log; private readonly ILogger<HonorificService> _logger;
private readonly IFramework _framework; private readonly IFramework _framework;
private bool _versionWarningLogged; private bool _versionWarningLogged;
@@ -34,12 +35,12 @@ internal sealed class HonorificService : IDisposable
public HonorificService( public HonorificService(
IDalamudPluginInterface pluginInterface, IDalamudPluginInterface pluginInterface,
IPluginLog log, ILogger<HonorificService> logger,
IFramework framework IFramework framework
) )
{ {
_framework = framework; _framework = framework;
_log = log; _logger = logger;
// Gate objects are cached per-name by Dalamud and safe to register // Gate objects are cached per-name by Dalamud and safe to register
// before Honorific loads — they just won't fire until it does. // before Honorific loads — they just won't fire until it does.
@@ -84,7 +85,7 @@ internal sealed class HonorificService : IDisposable
{ {
if (!_versionWarningLogged) if (!_versionWarningLogged)
{ {
_log.Warning( _logger.LogWarning(
"Honorific API version mismatch — expected major 3, " "Honorific API version mismatch — expected major 3, "
+ "found {Major}.{Minor}. Disabling Honorific integration.", + "found {Major}.{Minor}. Disabling Honorific integration.",
version.Item1, version.Item1,
@@ -104,7 +105,7 @@ internal sealed class HonorificService : IDisposable
catch (Exception ex) catch (Exception ex)
{ {
// Honorific not installed or not yet initialised — Ready will retry. // Honorific not installed or not yet initialised — Ready will retry.
_log.Debug(ex, "Honorific not available at HellionChat startup; awaiting Ready."); _logger.LogDebug(ex, "Honorific not available at HellionChat startup; awaiting Ready.");
IsAvailable = false; IsAvailable = false;
CurrentTitle = null; CurrentTitle = null;
} }
@@ -149,7 +150,7 @@ internal sealed class HonorificService : IDisposable
{ {
// Warning not Debug — a silent unsubscribe failure leaks a live // Warning not Debug — a silent unsubscribe failure leaks a live
// subscription across plugin reloads. // subscription across plugin reloads.
_log.Warning( _logger.LogWarning(
ex, ex,
"Honorific unsubscribe failed (likely API break or gate already gone)." "Honorific unsubscribe failed (likely API break or gate already gone)."
); );
+6 -5
View File
@@ -1,9 +1,12 @@
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ipc; namespace HellionChat.Ipc;
public sealed class ExtraChat : IDisposable public sealed class ExtraChat : IDisposable
{ {
private readonly ILogger<ExtraChat> _logger;
#pragma warning disable CS0649 // Assigned through IPC #pragma warning disable CS0649 // Assigned through IPC
[Serializable] [Serializable]
private struct OverrideInfo private struct OverrideInfo
@@ -36,8 +39,9 @@ public sealed class ExtraChat : IDisposable
private volatile Dictionary<Guid, string> ChannelNamesInternal = new(); private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal; internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
internal ExtraChat() internal ExtraChat(ILogger<ExtraChat> logger)
{ {
_logger = logger;
OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>( OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>(
"ExtraChat.OverrideChannelColour" "ExtraChat.OverrideChannelColour"
); );
@@ -62,10 +66,7 @@ public sealed class ExtraChat : IDisposable
catch (Exception ex) catch (Exception ex)
{ {
// ExtraChat is optional; IPC failure is normal when the plugin isn't loaded. // ExtraChat is optional; IPC failure is normal when the plugin isn't loaded.
Plugin.LogProxy.Verbose( _logger.LogTrace(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
ex,
"ExtraChat IPC initial state query failed (peer not loaded?)"
);
} }
} }
+5 -1
View File
@@ -1,5 +1,6 @@
using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc;
using HellionChat.Code; using HellionChat.Code;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ipc; namespace HellionChat.Ipc;
@@ -33,9 +34,12 @@ internal sealed class TypingIpc : IDisposable
private ChatInputState LastState; private ChatInputState LastState;
private bool HasState; private bool HasState;
internal TypingIpc(Plugin plugin) private readonly ILogger<TypingIpc> _logger;
internal TypingIpc(Plugin plugin, ILogger<TypingIpc> logger)
{ {
Plugin = plugin; Plugin = plugin;
_logger = logger;
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>( StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
"HellionChat.GetChatInputState" "HellionChat.GetChatInputState"
+5 -1
View File
@@ -1,11 +1,14 @@
using Dalamud.Game.Text.SeStringHandling; 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;
using Microsoft.Extensions.Logging;
namespace HellionChat; namespace HellionChat;
internal sealed class IpcManager : IDisposable internal sealed class IpcManager : IDisposable
{ {
private readonly ILogger<IpcManager> _logger;
private ICallGateProvider<string> RegisterGate { get; } private ICallGateProvider<string> RegisterGate { get; }
private ICallGateProvider<string, object?> UnregisterGate { get; } private ICallGateProvider<string, object?> UnregisterGate { get; }
private ICallGateProvider<object?> AvailableGate { get; } private ICallGateProvider<object?> AvailableGate { get; }
@@ -41,8 +44,9 @@ internal sealed class IpcManager : IDisposable
internal List<string> Registered { get; } = []; internal List<string> Registered { get; } = [];
public IpcManager() public IpcManager(ILogger<IpcManager> logger)
{ {
_logger = logger;
RegisterGate = Plugin.Interface.GetIpcProvider<string>("HellionChat.Register"); RegisterGate = Plugin.Interface.GetIpcProvider<string>("HellionChat.Register");
RegisterGate.RegisterFunc(Register); RegisterGate.RegisterFunc(Register);
+21 -9
View File
@@ -14,6 +14,7 @@ using HellionChat.Util;
using Lumina.Text.Expressions; using Lumina.Text.Expressions;
using Lumina.Text.Payloads; using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using Microsoft.Extensions.Logging;
namespace HellionChat; namespace HellionChat;
@@ -22,6 +23,7 @@ internal class MessageManager : IAsyncDisposable
internal const int MessageDisplayLimit = 10_000; internal const int MessageDisplayLimit = 10_000;
private Plugin Plugin { get; } private Plugin Plugin { get; }
private readonly ILogger<MessageManager> _logger;
internal MessageStore Store { get; } internal MessageStore Store { get; }
private Dictionary<ChatType, NameFormatting> Formats { get; } = []; private Dictionary<ChatType, NameFormatting> Formats { get; } = [];
@@ -48,11 +50,21 @@ internal class MessageManager : IAsyncDisposable
// AutoTellTabsService to spawn or refresh temp tabs without coupling. // AutoTellTabsService to spawn or refresh temp tabs without coupling.
public event Action<Message>? MessageProcessed; public event Action<Message>? MessageProcessed;
internal unsafe MessageManager(Plugin plugin) internal unsafe MessageManager(
Plugin plugin,
ILogger<MessageManager> logger,
ILoggerFactory loggerFactory
)
{ {
Plugin = plugin; Plugin = plugin;
_logger = logger;
Store = new MessageStore(DatabasePath(), Plugin.PlatformUtil, Plugin.LogProxy); Store = new MessageStore(
DatabasePath(),
Plugin.PlatformUtil,
loggerFactory.CreateLogger<MessageStore>(),
loggerFactory
);
PendingMessageThread = new Thread(() => PendingMessageThread = new Thread(() =>
ProcessPendingMessages(PendingThreadCancellationToken.Token) ProcessPendingMessages(PendingThreadCancellationToken.Token)
@@ -91,7 +103,7 @@ internal class MessageManager : IAsyncDisposable
await Task.Delay(100); await Task.Delay(100);
if (PendingMessageThread.IsAlive) if (PendingMessageThread.IsAlive)
Plugin.LogProxy.Warning( _logger.LogWarning(
"PendingMessageThread did not observe cancellation within 10s. " "PendingMessageThread did not observe cancellation within 10s. "
+ "Worker remains on background thread; next plugin reload releases it." + "Worker remains on background thread; next plugin reload releases it."
); );
@@ -137,7 +149,7 @@ internal class MessageManager : IAsyncDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error processing pending message"); _logger.LogError(ex, "Error processing pending message");
} }
} }
else else
@@ -182,12 +194,12 @@ internal class MessageManager : IAsyncDisposable
// Mark failed messages as deleted to prevent retry attempts // Mark failed messages as deleted to prevent retry attempts
var failedIds = messages.FailedMessageIds(); var failedIds = messages.FailedMessageIds();
Plugin.LogProxy.Info( _logger.LogInformation(
$"Marking {failedIds.Count} messages as deleted due to parse failures" $"Marking {failedIds.Count} messages as deleted due to parse failures"
); );
foreach (var msgId in messages.FailedMessageIds()) foreach (var msgId in messages.FailedMessageIds())
{ {
Plugin.LogProxy.Debug($"Marking message '{msgId}' as deleted due to parse failure"); _logger.LogDebug($"Marking message '{msgId}' as deleted due to parse failure");
Store.DeleteMessage(msgId); Store.DeleteMessage(msgId);
} }
} }
@@ -203,13 +215,13 @@ internal class MessageManager : IAsyncDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error in FilterAllTabs"); _logger.LogError(ex, "Error in FilterAllTabs");
} }
// v1.4.9 R3 profiling: Information so the xllog tail surfaces this // v1.4.9 R3 profiling: Information so the xllog tail surfaces this
// without a Debug filter. Belt-and-suspenders for future plugin-load // without a Debug filter. Belt-and-suspenders for future plugin-load
// regressions; remains in place after Sub-Task 3.4 Befund. // regressions; remains in place after Sub-Task 3.4 Befund.
Plugin.LogProxy.Information($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms"); _logger.LogInformation($"FilterAllTabs took {stopwatch.ElapsedMilliseconds}ms");
}); });
} }
@@ -264,7 +276,7 @@ internal class MessageManager : IAsyncDisposable
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error in ContentIdResolver"); _logger.LogError(ex, "Error in ContentIdResolver");
} }
} }
+46 -21
View File
@@ -9,6 +9,7 @@ using MessagePack;
using MessagePack.Formatters; using MessagePack.Formatters;
using MessagePack.Resolvers; using MessagePack.Resolvers;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using Encoding = System.Text.Encoding; using Encoding = System.Text.Encoding;
namespace HellionChat; namespace HellionChat;
@@ -179,7 +180,8 @@ internal class MessageStore : IDisposable
} }
private readonly IPlatformUtil _platformUtil; private readonly IPlatformUtil _platformUtil;
private readonly IPluginLogProxy _logger; private readonly ILogger<MessageStore> _logger;
private readonly ILoggerFactory _loggerFactory;
// Readiness gate for the FTS5 full-text index. Volatile so the DbViewer's // Readiness gate for the FTS5 full-text index. Volatile so the DbViewer's
// per-frame IsFtsIndexBuilt read sees the flip the moment the bulk-insert // per-frame IsFtsIndexBuilt read sees the flip the moment the bulk-insert
@@ -197,11 +199,17 @@ internal class MessageStore : IDisposable
// own SqliteConnection via OpenSecondaryConnection. // own SqliteConnection via OpenSecondaryConnection.
private readonly object _readLock = new(); private readonly object _readLock = new();
internal MessageStore(string dbPath, IPlatformUtil platformUtil, IPluginLogProxy logger) internal MessageStore(
string dbPath,
IPlatformUtil platformUtil,
ILogger<MessageStore> logger,
ILoggerFactory loggerFactory
)
{ {
DbPath = dbPath; DbPath = dbPath;
_platformUtil = platformUtil; _platformUtil = platformUtil;
_logger = logger; _logger = logger;
_loggerFactory = loggerFactory;
Connection = Connect(); Connection = Connect();
Migrate(); Migrate();
InitFtsReadyCache(); InitFtsReadyCache();
@@ -246,7 +254,7 @@ internal class MessageStore : IDisposable
conn.Open(); conn.Open();
ApplyPragmas(conn); ApplyPragmas(conn);
connectSw.Stop(); connectSw.Stop();
_logger.Information($"MessageStore.Connect took {connectSw.ElapsedMilliseconds}ms"); _logger.LogInformation($"MessageStore.Connect took {connectSw.ElapsedMilliseconds}ms");
return conn; return conn;
} }
@@ -290,12 +298,12 @@ internal class MessageStore : IDisposable
migration(); migration();
migrateSw.Stop(); migrateSw.Stop();
_logger.Information($"MessageStore.Migrate took {migrateSw.ElapsedMilliseconds}ms"); _logger.LogInformation($"MessageStore.Migrate took {migrateSw.ElapsedMilliseconds}ms");
} }
private void Migrate0() private void Migrate0()
{ {
_logger.Information("Running migration 0: Creating tables"); _logger.LogInformation("Running migration 0: Creating tables");
Connection.Execute( Connection.Execute(
@" @"
CREATE TABLE IF NOT EXISTS messages ( CREATE TABLE IF NOT EXISTS messages (
@@ -322,7 +330,7 @@ internal class MessageStore : IDisposable
private void Migrate1() private void Migrate1()
{ {
_logger.Information("Running migration 1: Adding Deleted column"); _logger.LogInformation("Running migration 1: Adding Deleted column");
Connection.Execute( Connection.Execute(
@" @"
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false; ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
@@ -334,7 +342,7 @@ internal class MessageStore : IDisposable
private void Migrate2() private void Migrate2()
{ {
_logger.Information("Running migration 2: Adding Channel generated column"); _logger.LogInformation("Running migration 2: Adding Channel generated column");
Connection.Execute( Connection.Execute(
@" @"
ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL; ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL;
@@ -362,13 +370,15 @@ internal class MessageStore : IDisposable
private void Migrate3() private void Migrate3()
{ {
_logger.Information("Running migration 3: Fix log kinds to fit the new format"); _logger.LogInformation("Running migration 3: Fix log kinds to fit the new format");
// Recovery for partially-applied Migrate3: schema already in target // Recovery for partially-applied Migrate3: schema already in target
// shape but user_version was never bumped -- just record and exit. // shape but user_version was never bumped -- just record and exit.
if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code")) if (ColumnExists("messages", "ChatType") && !ColumnExists("messages", "Code"))
{ {
_logger.Information("Migration 3: schema already migrated, only bumping user_version"); _logger.LogInformation(
"Migration 3: schema already migrated, only bumping user_version"
);
SetMigrationVersion(3); SetMigrationVersion(3);
return; return;
} }
@@ -398,7 +408,7 @@ internal class MessageStore : IDisposable
private void Migrate4() private void Migrate4()
{ {
_logger.Information("Running migration 4: Add FTS5 virtual table for full-text search"); _logger.LogInformation("Running migration 4: Add FTS5 virtual table for full-text search");
// Standalone FTS5 table (no content='messages' linking, no content_rowid). // Standalone FTS5 table (no content='messages' linking, no content_rowid).
// messages.Id is BLOB-PK (Guid), which is incompatible with FTS5's // messages.Id is BLOB-PK (Guid), which is incompatible with FTS5's
@@ -422,7 +432,7 @@ internal class MessageStore : IDisposable
private void SetMigrationVersion(int version) private void SetMigrationVersion(int version)
{ {
_logger.Information($"Setting version {version}"); _logger.LogInformation($"Setting version {version}");
using var cmd = Connection.CreateCommand(); using var cmd = Connection.CreateCommand();
// PRAGMA does not accept SQLite parameter bindings; version is a // PRAGMA does not accept SQLite parameter bindings; version is a
// compile-time int from the migration sequence, never user input. // compile-time int from the migration sequence, never user input.
@@ -837,7 +847,7 @@ internal class MessageStore : IDisposable
// Privacy filter -- drop disallowed ChatTypes before they reach storage. // Privacy filter -- drop disallowed ChatTypes before they reach storage.
if (!Plugin.Config.IsAllowedForStorage(message.Code.Type)) if (!Plugin.Config.IsAllowedForStorage(message.Code.Type))
{ {
_logger.Verbose($"Privacy filter dropped message: ChatType={message.Code.Type}"); _logger.LogTrace($"Privacy filter dropped message: ChatType={message.Code.Type}");
return; return;
} }
@@ -941,7 +951,10 @@ internal class MessageStore : IDisposable
if (to is not null) if (to is not null)
cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds()); cmd.Parameters.AddWithValue("$To", to.Value.ToUnixTimeMilliseconds());
return new MessageEnumerator(cmd.ExecuteReader(), _logger); return new MessageEnumerator(
cmd.ExecuteReader(),
_loggerFactory.CreateLogger<MessageEnumerator>()
);
} }
} }
@@ -993,7 +1006,10 @@ internal class MessageStore : IDisposable
cmd.Parameters.AddWithValue("$Count", count); cmd.Parameters.AddWithValue("$Count", count);
return new MessageEnumerator(cmd.ExecuteReader(), _logger); return new MessageEnumerator(
cmd.ExecuteReader(),
_loggerFactory.CreateLogger<MessageEnumerator>()
);
} }
} }
@@ -1033,7 +1049,10 @@ internal class MessageStore : IDisposable
cmd.Parameters.AddWithValue("$TellOutgoing", (int)ChatType.TellOutgoing); cmd.Parameters.AddWithValue("$TellOutgoing", (int)ChatType.TellOutgoing);
var collected = new List<Message>(); var collected = new List<Message>();
using var enumerator = new MessageEnumerator(cmd.ExecuteReader(), _logger); using var enumerator = new MessageEnumerator(
cmd.ExecuteReader(),
_loggerFactory.CreateLogger<MessageEnumerator>()
);
foreach (var message in enumerator) foreach (var message in enumerator)
{ {
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld)) if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
@@ -1145,7 +1164,10 @@ internal class MessageStore : IDisposable
((DateTimeOffset)before).ToUnixTimeMilliseconds() ((DateTimeOffset)before).ToUnixTimeMilliseconds()
); );
return new MessageEnumerator(cmd.ExecuteReader(), _logger); return new MessageEnumerator(
cmd.ExecuteReader(),
_loggerFactory.CreateLogger<MessageEnumerator>()
);
} }
} }
@@ -1198,7 +1220,10 @@ internal class MessageStore : IDisposable
cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page); cmd.Parameters.AddWithValue("$Offset", DbViewer.RowPerPage * page);
cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage); cmd.Parameters.AddWithValue("$OffsetCount", DbViewer.RowPerPage);
return new MessageEnumerator(cmd.ExecuteReader(), _logger); return new MessageEnumerator(
cmd.ExecuteReader(),
_loggerFactory.CreateLogger<MessageEnumerator>()
);
} }
} }
@@ -1219,14 +1244,14 @@ internal class MessageStore : IDisposable
} }
} }
internal class MessageEnumerator(DbDataReader reader, IPluginLogProxy logger) internal class MessageEnumerator(DbDataReader reader, ILogger<MessageEnumerator> logger)
: IEnumerable<Message>, : IEnumerable<Message>,
IDisposable, IDisposable,
IAsyncDisposable IAsyncDisposable
{ {
private const int MaxErrorLogs = 10; private const int MaxErrorLogs = 10;
private readonly IPluginLogProxy _logger = logger; private readonly ILogger<MessageEnumerator> _logger = logger;
private readonly List<Guid> FailedIds = []; private readonly List<Guid> FailedIds = [];
private int FailedCount; private int FailedCount;
public bool DidError => FailedCount > 0; public bool DidError => FailedCount > 0;
@@ -1247,10 +1272,10 @@ internal class MessageEnumerator(DbDataReader reader, IPluginLogProxy logger)
catch (Exception e) catch (Exception e)
{ {
if (FailedCount < MaxErrorLogs) if (FailedCount < MaxErrorLogs)
_logger.Error($"Exception while reading message '{id}' from database: {e}"); _logger.LogError($"Exception while reading message '{id}' from database: {e}");
FailedCount++; FailedCount++;
if (FailedCount == MaxErrorLogs) if (FailedCount == MaxErrorLogs)
_logger.Error("Further parsing errors will not be logged"); _logger.LogError("Further parsing errors will not be logged");
if (id != Guid.Empty) if (id != Guid.Empty)
FailedIds.Add(id); FailedIds.Add(id);
+8 -4
View File
@@ -20,6 +20,7 @@ using HellionChat.Resources;
using HellionChat.Ui; using HellionChat.Ui;
using HellionChat.Util; using HellionChat.Util;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Microsoft.Extensions.Logging;
using Action = System.Action; using Action = System.Action;
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload; using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload; using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
@@ -40,9 +41,12 @@ public sealed class PayloadHandler
private const uint PopupSfx = 1; private const uint PopupSfx = 1;
internal PayloadHandler(ChatLogWindow logWindow) private readonly ILogger<PayloadHandler> _logger;
internal PayloadHandler(ChatLogWindow logWindow, ILogger<PayloadHandler> logger)
{ {
LogWindow = logWindow; LogWindow = logWindow;
_logger = logger;
} }
internal void Draw() internal void Draw()
@@ -131,7 +135,7 @@ public sealed class PayloadHandler
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error executing integration"); _logger.LogError(ex, "Error executing integration");
} }
} }
@@ -535,7 +539,7 @@ public sealed class PayloadHandler
) )
) )
{ {
Plugin.LogProxy.Warning("Could not find DalamudLinkHandlers"); _logger.LogWarning("Could not find DalamudLinkHandlers");
return; return;
} }
@@ -546,7 +550,7 @@ public sealed class PayloadHandler
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error executing DalamudLinkPayload handler"); _logger.LogError(ex, "Error executing DalamudLinkPayload handler");
} }
} }
+99 -100
View File
@@ -15,6 +15,8 @@ using HellionChat.Resources;
using HellionChat.Ui; using HellionChat.Ui;
using HellionChat.Util; using HellionChat.Util;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace HellionChat; namespace HellionChat;
@@ -123,6 +125,11 @@ public sealed class Plugin : IAsyncDalamudPlugin
// isolation. Wired immediately after Dalamud injects Log. // isolation. Wired immediately after Dalamud injects Log.
internal static IPluginLogProxy LogProxy { get; private set; } = null!; internal static IPluginLogProxy LogProxy { get; private set; } = null!;
// Nullable so DisposeAsync stays safe if Host-build throws before the
// fields get assigned — Dalamud fires DisposeAsync regardless.
private readonly IHost? _host;
private readonly PluginLifecycle? _lifecycle;
// Wrapper cached so TearDown can detach the live instance instead of // Wrapper cached so TearDown can detach the live instance instead of
// re-registering with identical args (v1.4.9 ISSUE-1 cleanup). // re-registering with identical args (v1.4.9 ISSUE-1 cleanup).
private CommandWrapper? _hellionSettingsCmd; private CommandWrapper? _hellionSettingsCmd;
@@ -185,11 +192,11 @@ public sealed class Plugin : IAsyncDalamudPlugin
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration(); Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
// Wire platform indirection before LoadAsync allocates anything that // PlatformUtil and LogProxy are filled from the DI container in
// needs Util.* — services then read Plugin.PlatformUtil instead of // Phase-1 below (`_host.Services.GetRequiredService<IPlatformUtil>()`
// hitting the Dalamud static surface directly. // and the LogProxy equivalent). Phase-0 helpers that run before that
PlatformUtil = new DalamudPlatformUtil(); // point (MigrateFromChatTwoLayout, LanguageChanged, ImGuiUtil.Initialize)
LogProxy = new DalamudPluginLogProxy(Log); // do not touch either static, so the brief null-window is safe.
// Schema gate: v1.4.x requires config v16+. Users on older schemas // Schema gate: v1.4.x requires config v16+. Users on older schemas
// must install v1.4.2 first to run the migration chain. v17 adds // must install v1.4.2 first to run the migration chain. v17 adds
@@ -212,6 +219,72 @@ public sealed class Plugin : IAsyncDalamudPlugin
ImGuiUtil.Initialize(this); ImGuiUtil.Initialize(this);
DeferredSaveFrames = -1; DeferredSaveFrames = -1;
// Custom themes dir + seed run before the container builds so the
// ThemeRegistry factory lambda finds the directory ready.
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
Directory.CreateDirectory(customThemesDir);
SeedExampleThemeIfEmpty(customThemesDir);
// Phase-1: build the host synchronously (the schema gate must clear
// before services allocate; Lightless' deferred build would invert
// that order) and pull singletons into the Plugin.X surface.
var dependencies = new PluginHostDependencies(
Interface,
Log,
ChatGui,
ClientState,
CommandManager,
Condition,
DataManager,
Framework,
GameGui,
KeyState,
ObjectTable,
PartyList,
TargetManager,
TextureProvider,
GameInteropProvider,
GameConfig,
Notification,
AddonLifecycle,
PlayerState,
Evaluator,
SelfTestRegistry
);
_host = PluginHostFactory.Build(this, dependencies);
_lifecycle = _host.Services.GetRequiredService<PluginLifecycle>();
_lifecycle.Host = _host;
// Plugin.X static bridge - filled from the container so DI-aware code
// and the ~93 Plugin.X consumer sites read the same instances.
PlatformUtil = _host.Services.GetRequiredService<IPlatformUtil>();
LogProxy = _host.Services.GetRequiredService<IPluginLogProxy>();
FileDialogManager = _host.Services.GetRequiredService<FileDialogManager>();
// Resolve order matters: block-B services first so the windows can
// read Plugin.MessageManager etc. from their own ctors without NREs.
FontManager = _host.Services.GetRequiredService<FontManager>();
ThemeRegistry = _host.Services.GetRequiredService<Themes.ThemeRegistry>();
Commands = _host.Services.GetRequiredService<Commands>();
Functions = _host.Services.GetRequiredService<GameFunctions.GameFunctions>();
Ipc = _host.Services.GetRequiredService<IpcManager>();
TypingIpc = _host.Services.GetRequiredService<TypingIpc>();
ExtraChat = _host.Services.GetRequiredService<ExtraChat>();
HonorificService = _host.Services.GetRequiredService<Integrations.HonorificService>();
StatusBar = _host.Services.GetRequiredService<Ui.StatusBar>();
MessageManager = _host.Services.GetRequiredService<MessageManager>();
AutoTellTabsService = _host.Services.GetRequiredService<AutoTellTabsService>();
ChatLogWindow = _host.Services.GetRequiredService<ChatLogWindow>();
SettingsWindow = _host.Services.GetRequiredService<SettingsWindow>();
DbViewer = _host.Services.GetRequiredService<DbViewer>();
InputPreview = _host.Services.GetRequiredService<InputPreview>();
CommandHelpWindow = _host.Services.GetRequiredService<CommandHelpWindow>();
SeStringDebugger = _host.Services.GetRequiredService<SeStringDebugger>();
DebuggerWindow = _host.Services.GetRequiredService<DebuggerWindow>();
FirstRunWizard = _host.Services.GetRequiredService<FirstRunWizard>();
} }
public async Task LoadAsync(CancellationToken cancellationToken) public async Task LoadAsync(CancellationToken cancellationToken)
@@ -233,66 +306,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
// BuildFonts registers handles with Dalamud's FontAtlas; the atlas // Container drives service init now: Host.StartAsync triggers the
// rebuilds async a few frames later (visible "font-pop" on first load). // IHostedService adapters (FontManager.BuildFonts, ThemeRegistry
FontManager = new FontManager(); // cache warmup + Switch, IPC eager-resolve, MessageManager
FontManager.BuildFonts(); // FilterAllTabsAsync, AutoTellTabsService.Initialize). Window
// registration with WindowSystem runs on the framework thread
// ThemeRegistry must be wired before the first Draw tick. // inside PluginLifecycle.LoadAsync after StartAsync returns.
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes"); if (_lifecycle is not null)
Directory.CreateDirectory(customThemesDir); await _lifecycle.LoadAsync(cancellationToken).ConfigureAwait(false);
SeedExampleThemeIfEmpty(customThemesDir);
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
// Warm up the custom-theme cache before the first Switch.
// LoadCustomBySlug is a reverse-lookup over _customCache; on a
// cold cache a Config.Theme that points at a custom slug would
// fall through to the built-in default. AllCustom is a lazy
// enumerable, so iterate it explicitly to materialise the cache.
foreach (var _ in ThemeRegistry.AllCustom()) { }
ThemeRegistry.Switch(Config.Theme);
cancellationToken.ThrowIfCancellationRequested();
// Service allocations — order encodes dependencies.
// HonorificService registers IPC subscribers early to catch
// Ready/Disposing events from the first frame.
FileDialogManager = new FileDialogManager();
Commands = new Commands();
Functions = new GameFunctions.GameFunctions(this);
Ipc = new IpcManager();
TypingIpc = new TypingIpc(this);
ExtraChat = new ExtraChat();
HonorificService = new Integrations.HonorificService(Interface, Log, Framework);
StatusBar = new Ui.StatusBar();
MessageManager = new MessageManager(this);
AutoTellTabsService = new AutoTellTabsService(
this,
MessageManager,
MessageManager.Store
);
AutoTellTabsService.Initialize();
SelfTestRegistry.RegisterTestSteps([new SelfTests.ThemeSwitchSelfTestStep(this)]); SelfTestRegistry.RegisterTestSteps([new SelfTests.ThemeSwitchSelfTestStep(this)]);
ChatLogWindow = new ChatLogWindow(this);
SettingsWindow = new SettingsWindow(this);
DbViewer = new DbViewer(this);
InputPreview = new InputPreview(ChatLogWindow);
CommandHelpWindow = new CommandHelpWindow(ChatLogWindow);
SeStringDebugger = new SeStringDebugger(this);
DebuggerWindow = new DebuggerWindow(this);
FirstRunWizard = new FirstRunWizard(this);
WindowSystem.AddWindow(ChatLogWindow);
WindowSystem.AddWindow(SettingsWindow);
WindowSystem.AddWindow(DbViewer);
WindowSystem.AddWindow(InputPreview);
WindowSystem.AddWindow(CommandHelpWindow);
WindowSystem.AddWindow(SeStringDebugger);
WindowSystem.AddWindow(DebuggerWindow);
WindowSystem.AddWindow(FirstRunWizard);
if (!Config.FirstRunCompleted) if (!Config.FirstRunCompleted)
FirstRunWizard.IsOpen = true; FirstRunWizard.IsOpen = true;
@@ -313,8 +337,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
if (Config.ShowEmotes) if (Config.ShowEmotes)
_ = EmoteCache.LoadData(); _ = EmoteCache.LoadData();
if (Interface.Reason is not PluginLoadReason.Boot) // FilterAllTabsAsync now runs from MessageManagerInitHostedService
MessageManager.FilterAllTabsAsync(); // during Host.StartAsync (same Reason-not-Boot guard there).
// Kick the FTS5 rebuild worker if Migrate4 just added the schema or // Kick the FTS5 rebuild worker if Migrate4 just added the schema or
// a previous run was cut short (InitFtsReadyCache leaves _ftsReady // a previous run was cut short (InitFtsReadyCache leaves _ftsReady
@@ -499,49 +523,18 @@ public sealed class Plugin : IAsyncDalamudPlugin
} }
); );
// Unsubscribe AutoTellTabs before MessageManager goes away. // Framework-thread cleanup the container does not reach.
failure = CaptureFailure(failure, () => AutoTellTabsService?.Dispose());
// MessageManager has its own async dispose path (DB flush, thread shutdown).
if (MessageManager is not null)
{
failure = await CaptureFailureAsync(
failure,
() => MessageManager.DisposeAsync().AsTask()
)
.ConfigureAwait(false);
}
// Game-function / IPC / window cleanup must run on the framework thread.
try try
{ {
await Framework await Framework
.RunOnFrameworkThread(() => .RunOnFrameworkThread(() =>
{ {
// TearDown slash-commands + UiBuilder hooks before windows
// tear down. Slash-commands holding handlers that reach
// the windows would otherwise see a half-torn Plugin.
failure = CaptureFailure(failure, TearDownCommands); failure = CaptureFailure(failure, TearDownCommands);
failure = CaptureFailure( failure = CaptureFailure(
failure, failure,
() => GameFunctions.GameFunctions.SetChatInteractable(true) () => GameFunctions.GameFunctions.SetChatInteractable(true)
); );
// IPC subscribers before windows — prevents a final IPC event
// from reaching a half-torn ChatLogWindow.
failure = CaptureFailure(failure, () => HonorificService?.Dispose());
failure = CaptureFailure(failure, () => TypingIpc?.Dispose());
failure = CaptureFailure(failure, () => ExtraChat?.Dispose());
failure = CaptureFailure(failure, () => Ipc?.Dispose());
failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows()); failure = CaptureFailure(failure, () => WindowSystem?.RemoveAllWindows());
failure = CaptureFailure(failure, () => ChatLogWindow?.Dispose());
failure = CaptureFailure(failure, () => DbViewer?.Dispose());
failure = CaptureFailure(failure, () => InputPreview?.Dispose());
failure = CaptureFailure(failure, () => SettingsWindow?.Dispose());
failure = CaptureFailure(failure, () => DebuggerWindow?.Dispose());
failure = CaptureFailure(failure, () => SeStringDebugger?.Dispose());
}) })
.ConfigureAwait(false); .ConfigureAwait(false);
} }
@@ -550,11 +543,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
failure ??= ex; failure ??= ex;
} }
// Pure-memory cleanups — no Framework / UI / IPC touch. // Container disposes services + windows on the framework thread.
failure = CaptureFailure(failure, () => Functions?.Dispose()); // MessageManager.DisposeAsync is not idempotent, so we let the
failure = CaptureFailure(failure, () => Commands?.Dispose()); // container do it once instead of double-disposing.
if (_lifecycle is not null)
{
failure = await CaptureFailureAsync(failure, () => _lifecycle.DisposeAsync().AsTask())
.ConfigureAwait(false);
}
// Static-class cleanups the container has no handle on.
failure = CaptureFailure(failure, () => EmoteCache.Dispose()); failure = CaptureFailure(failure, () => EmoteCache.Dispose());
// Static input history would otherwise survive the plugin reload.
failure = CaptureFailure(failure, InputHistoryService.Reset); failure = CaptureFailure(failure, InputHistoryService.Reset);
if (failure is not null) if (failure is not null)
+199
View File
@@ -0,0 +1,199 @@
using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using HellionChat.Infrastructure.Hosting;
using HellionChat.Infrastructure.Logging;
using HellionChat.Ipc;
using HellionChat.Themes;
using HellionChat.Ui;
using HellionChat.Util;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace HellionChat;
// Builds the generic-host DI container that drives v1.5.0+. The factory is
// invoked synchronously from Plugin.ctor (after the schema gate clears) so the
// container exists before PluginLifecycle.LoadAsync runs. See plan §1 for the
// deliberate divergence from Lightless' deferred Func-delegate pattern.
internal static class PluginHostFactory
{
public static IHost Build(Plugin plugin, PluginHostDependencies dependencies)
{
return new HostBuilder()
.UseContentRoot(dependencies.PluginInterface.ConfigDirectory.FullName)
.ConfigureLogging(logging =>
{
logging.ClearProviders();
logging.AddDalamudLogging(dependencies.PluginLog);
logging.SetMinimumLevel(LogLevel.Trace);
})
.ConfigureServices(services => ConfigureServices(services, plugin, dependencies))
.Build();
}
private static void ConfigureServices(
IServiceCollection services,
Plugin plugin,
PluginHostDependencies dependencies
)
{
// Block A — Dalamud services (21 [PluginService] singletons).
services.AddSingleton(dependencies);
services.AddSingleton(dependencies.PluginInterface);
services.AddSingleton(dependencies.PluginLog);
services.AddSingleton(dependencies.ChatGui);
services.AddSingleton(dependencies.ClientState);
services.AddSingleton(dependencies.CommandManager);
services.AddSingleton(dependencies.Condition);
services.AddSingleton(dependencies.DataManager);
services.AddSingleton(dependencies.Framework);
services.AddSingleton(dependencies.GameGui);
services.AddSingleton(dependencies.KeyState);
services.AddSingleton(dependencies.ObjectTable);
services.AddSingleton(dependencies.PartyList);
services.AddSingleton(dependencies.TargetManager);
services.AddSingleton(dependencies.TextureProvider);
services.AddSingleton(dependencies.GameInteropProvider);
services.AddSingleton(dependencies.GameConfig);
services.AddSingleton(dependencies.Notification);
services.AddSingleton(dependencies.AddonLifecycle);
services.AddSingleton(dependencies.PlayerState);
services.AddSingleton(dependencies.Evaluator);
services.AddSingleton(dependencies.SelfTestRegistry);
// Self-references: Plugin and its WindowSystem already exist.
services.AddSingleton(plugin);
services.AddSingleton(plugin.WindowSystem);
services.AddSingleton<PluginLifecycle>();
// Block B — HellionChat singletons. Factory lambdas because most
// classes are internal-sealed and the default activator only sees
// public ctors.
services.AddSingleton<IPlatformUtil>(_ => new DalamudPlatformUtil());
services.AddSingleton<IPluginLogProxy>(sp => new DalamudPluginLogProxy(
sp.GetRequiredService<IPluginLog>()
));
services.AddSingleton<FileDialogManager>(_ => new FileDialogManager());
services.AddSingleton(sp => new Commands(sp.GetRequiredService<ILogger<Commands>>()));
services.AddSingleton(_ => new FontManager());
services.AddSingleton(_ => new StatusBar());
services.AddSingleton(sp => new IpcManager(sp.GetRequiredService<ILogger<IpcManager>>()));
services.AddSingleton(sp => new ExtraChat(sp.GetRequiredService<ILogger<ExtraChat>>()));
services.AddSingleton(sp => new ThemeRegistry(
Path.Combine(
sp.GetRequiredService<IDalamudPluginInterface>().ConfigDirectory.FullName,
"themes"
),
sp.GetRequiredService<ILogger<ThemeRegistry>>()
));
services.AddSingleton(sp => new GameFunctions.GameFunctions(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILogger<GameFunctions.GameFunctions>>(),
sp.GetRequiredService<ILoggerFactory>()
));
services.AddSingleton(sp => new TypingIpc(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILogger<TypingIpc>>()
));
services.AddSingleton(sp => new Integrations.HonorificService(
sp.GetRequiredService<IDalamudPluginInterface>(),
sp.GetRequiredService<ILogger<Integrations.HonorificService>>(),
sp.GetRequiredService<IFramework>()
));
services.AddSingleton(sp => new MessageManager(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILogger<MessageManager>>(),
sp.GetRequiredService<ILoggerFactory>()
));
// MessageStore is allocated inside MessageManager.ctor; a separate
// container singleton would double-construct the SQLite handle.
services.AddSingleton(sp =>
{
var pluginRef = sp.GetRequiredService<Plugin>();
var manager = sp.GetRequiredService<MessageManager>();
return new AutoTellTabsService(
pluginRef,
manager,
manager.Store,
sp.GetRequiredService<ILogger<AutoTellTabsService>>()
);
});
// Block C — Windows. WindowSystem.AddWindow is called from
// PluginLifecycle.LoadAsync on the framework thread.
services.AddSingleton(sp => new ChatLogWindow(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILogger<ChatLogWindow>>(),
sp.GetRequiredService<ILoggerFactory>()
));
services.AddSingleton(sp => new SettingsWindow(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILoggerFactory>()
));
services.AddSingleton(sp => new DbViewer(
sp.GetRequiredService<Plugin>(),
sp.GetRequiredService<ILogger<DbViewer>>()
));
services.AddSingleton(sp => new InputPreview(sp.GetRequiredService<ChatLogWindow>()));
services.AddSingleton(sp => new CommandHelpWindow(sp.GetRequiredService<ChatLogWindow>()));
services.AddSingleton(sp => new SeStringDebugger(sp.GetRequiredService<Plugin>()));
services.AddSingleton(sp => new DebuggerWindow(sp.GetRequiredService<Plugin>()));
services.AddSingleton(sp => new FirstRunWizard(sp.GetRequiredService<Plugin>()));
// Hosted-service adapters: thin wrappers around the existing init
// methods so the service class bodies stay unchanged.
services.AddHostedService(sp => new FontManagerInitHostedService(
sp.GetRequiredService<FontManager>()
));
services.AddHostedService(sp => new ThemeRegistryInitHostedService(
sp.GetRequiredService<ThemeRegistry>()
));
services.AddHostedService(sp => new IpcManagerInitHostedService(
sp.GetRequiredService<IpcManager>()
));
services.AddHostedService(sp => new TypingIpcInitHostedService(
sp.GetRequiredService<TypingIpc>()
));
services.AddHostedService(sp => new ExtraChatInitHostedService(
sp.GetRequiredService<ExtraChat>()
));
services.AddHostedService(sp => new MessageManagerInitHostedService(
sp.GetRequiredService<IDalamudPluginInterface>(),
sp.GetRequiredService<MessageManager>()
));
services.AddHostedService(sp => new AutoTellTabsServiceInitHostedService(
sp.GetRequiredService<AutoTellTabsService>()
));
}
}
internal sealed record PluginHostDependencies(
IDalamudPluginInterface PluginInterface,
IPluginLog PluginLog,
IChatGui ChatGui,
IClientState ClientState,
ICommandManager CommandManager,
ICondition Condition,
IDataManager DataManager,
IFramework Framework,
IGameGui GameGui,
IKeyState KeyState,
IObjectTable ObjectTable,
IPartyList PartyList,
ITargetManager TargetManager,
ITextureProvider TextureProvider,
IGameInteropProvider GameInteropProvider,
IGameConfig GameConfig,
INotificationManager Notification,
IAddonLifecycle AddonLifecycle,
IPlayerState PlayerState,
ISeStringEvaluator Evaluator,
ISelfTestRegistry SelfTestRegistry
);
+143
View File
@@ -0,0 +1,143 @@
using System.Runtime.ExceptionServices;
using Dalamud.Plugin.Services;
using Microsoft.Extensions.Hosting;
namespace HellionChat;
// Orchestrates Host.StartAsync / StopAsync and the framework-thread dispose.
// Plugin.ctor builds the host and assigns it via the Host property, so
// PluginLifecycle never constructs the host itself.
internal sealed class PluginLifecycle : IAsyncDisposable
{
private readonly IFramework _framework;
private readonly Plugin _plugin;
private int _disposeStarted;
private bool _hostStartRequested;
public PluginLifecycle(IFramework framework, Plugin plugin)
{
_framework = framework;
_plugin = plugin;
}
// Plugin.ctor fills this immediately after PluginHostFactory.Build and
// before invoking LoadAsync; LoadAsync may NRE-suppress on Host! safely.
public IHost? Host { get; set; }
public async Task LoadAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
try
{
_hostStartRequested = true;
await Host!.StartAsync(cancellationToken).ConfigureAwait(false);
// WindowSystem.AddWindow mutates an internal List<>; v1.4.9 Stage-2
// verified the list is non-thread-safe, so we marshal the entire
// registration block to the framework thread.
await _framework
.RunOnFrameworkThread(() => RegisterWindows(_plugin))
.ConfigureAwait(false);
}
catch
{
try
{
await DisposeAsync().ConfigureAwait(false);
}
catch
{
// Swallow secondary dispose failure so the original load throw wins.
}
throw;
}
}
private static void RegisterWindows(Plugin plugin)
{
plugin.WindowSystem.AddWindow(plugin.ChatLogWindow);
plugin.WindowSystem.AddWindow(plugin.SettingsWindow);
plugin.WindowSystem.AddWindow(plugin.DbViewer);
plugin.WindowSystem.AddWindow(plugin.InputPreview);
plugin.WindowSystem.AddWindow(plugin.CommandHelpWindow);
plugin.WindowSystem.AddWindow(plugin.SeStringDebugger);
plugin.WindowSystem.AddWindow(plugin.DebuggerWindow);
plugin.WindowSystem.AddWindow(plugin.FirstRunWizard);
}
public async ValueTask DisposeAsync()
{
// Idempotency guard — Dalamud may fire DisposeAsync twice in a reload race.
if (Interlocked.Exchange(ref _disposeStarted, 1) != 0)
return;
Exception? failure = null;
if (_hostStartRequested && Host is not null)
failure = await CaptureFailureAsync(failure, () => Host.StopAsync())
.ConfigureAwait(false);
failure = await DisposeHostOnFrameworkThreadAsync(failure).ConfigureAwait(false);
ThrowIfFailed(failure);
}
private async Task<Exception?> DisposeHostOnFrameworkThreadAsync(Exception? failure)
{
try
{
await _framework
.RunOnFrameworkThread(() =>
{
failure = CaptureFailure(failure, () => Host?.Dispose());
})
.ConfigureAwait(false);
}
catch (Exception ex)
{
failure ??= ex;
}
return failure;
}
private static Exception? CaptureFailure(Exception? failure, Action action)
{
try
{
action();
}
catch (Exception ex)
{
failure ??= ex;
}
return failure;
}
private static async ValueTask<Exception?> CaptureFailureAsync(
Exception? failure,
Func<Task> action
)
{
try
{
await action().ConfigureAwait(false);
}
catch (Exception ex)
{
failure ??= ex;
}
return failure;
}
private static void ThrowIfFailed(Exception? failure)
{
if (failure is not null)
ExceptionDispatchInfo.Capture(failure).Throw();
}
}
+6 -2
View File
@@ -1,9 +1,12 @@
using HellionChat.Themes.Builtin; using HellionChat.Themes.Builtin;
using Microsoft.Extensions.Logging;
namespace HellionChat.Themes; namespace HellionChat.Themes;
public sealed class ThemeRegistry public sealed class ThemeRegistry
{ {
private readonly ILogger<ThemeRegistry>? _logger;
public const string DefaultSlug = HellionArctic.Slug; public const string DefaultSlug = HellionArctic.Slug;
// 1Hz throttle for the v1.4.8 B2 auto-refresh-on-active path. The // 1Hz throttle for the v1.4.8 B2 auto-refresh-on-active path. The
@@ -29,8 +32,9 @@ public sealed class ThemeRegistry
private long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs; private long _lastActiveStampCheckMs = -ActiveStampPollIntervalMs;
private DateTime _lastActiveStamp = DateTime.MinValue; private DateTime _lastActiveStamp = DateTime.MinValue;
public ThemeRegistry(string? customThemesDir = null) public ThemeRegistry(string? customThemesDir = null, ILogger<ThemeRegistry>? logger = null)
{ {
_logger = logger;
// Insertion order drives the Theme-Picker grid layout (3 columns). // Insertion order drives the Theme-Picker grid layout (3 columns).
// Row 1: blue family. Row 2: purple to magenta family. // Row 1: blue family. Row 2: purple to magenta family.
// Row 3: green / warm / classic. Row 4: Synthwave Sunset as a // Row 3: green / warm / classic. Row 4: Synthwave Sunset as a
@@ -206,7 +210,7 @@ public sealed class ThemeRegistry
catch (Exception ex) when (IsRecoverableFileLock(ex)) catch (Exception ex) when (IsRecoverableFileLock(ex))
{ {
// Editor mid-save: keep last known good, retry on next refresh. // Editor mid-save: keep last known good, retry on next refresh.
Plugin.LogProxy.Debug( _logger?.LogDebug(
$"Custom theme {Path.GetFileName(path)} is locked, keeping last known good" $"Custom theme {Path.GetFileName(path)} is locked, keeping last known good"
); );
if (cached.Theme is not null) if (cached.Theme is not null)
+50 -21
View File
@@ -22,6 +22,7 @@ using HellionChat.Resources;
using HellionChat.Util; using HellionChat.Util;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using Lumina.Extensions; using Lumina.Extensions;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui; namespace HellionChat.Ui;
@@ -98,10 +99,19 @@ public sealed class ChatLogWindow : Window
private long FrameTime; // set every frame private long FrameTime; // set every frame
internal long LastActivityTime = Environment.TickCount64; internal long LastActivityTime = Environment.TickCount64;
internal ChatLogWindow(Plugin plugin) private readonly ILogger<ChatLogWindow> _logger;
private readonly ILoggerFactory _loggerFactory;
internal ChatLogWindow(
Plugin plugin,
ILogger<ChatLogWindow> logger,
ILoggerFactory loggerFactory
)
: base($"{Plugin.PluginName}###chat2") : base($"{Plugin.PluginName}###chat2")
{ {
Plugin = plugin; Plugin = plugin;
_logger = logger;
_loggerFactory = loggerFactory;
Salt = new Random().Next().ToString(); Salt = new Random().Next().ToString();
Size = new Vector2(500, 250); Size = new Vector2(500, 250);
@@ -114,8 +124,10 @@ public sealed class ChatLogWindow : Window
DisableWindowSounds = true; DisableWindowSounds = true;
// AllowBackgroundBlur is set centrally in Plugin.Setup after AddWindow. // AllowBackgroundBlur is set centrally in Plugin.Setup after AddWindow.
PayloadHandler = new PayloadHandler(this); PayloadHandler = new PayloadHandler(this, _loggerFactory.CreateLogger<PayloadHandler>());
HandlerLender = new Lender<PayloadHandler>(() => new PayloadHandler(this)); HandlerLender = new Lender<PayloadHandler>(() =>
new PayloadHandler(this, _loggerFactory.CreateLogger<PayloadHandler>())
);
SetUpTextCommandChannels(); SetUpTextCommandChannels();
SetUpAllCommands(); SetUpAllCommands();
@@ -192,11 +204,28 @@ public sealed class ChatLogWindow : Window
return; return;
} }
// ---------------------------------------------------------------
// Cherry-picked from ChatTwo upstream ee7768ac (Infiziert90, 2026-05-16)
// - Replace the chat input when args.AddIfNotPresent / args.Input starts
// with a slash. Vanilla actions like the Friend List "/tell" entry and
// other plugins push slash commands through these args; appending them
// to existing text would produce inputs like "test/tell user@world".
// ---------------------------------------------------------------
if (args.AddIfNotPresent != null && !Chat.Contains(args.AddIfNotPresent)) if (args.AddIfNotPresent != null && !Chat.Contains(args.AddIfNotPresent))
Chat += args.AddIfNotPresent; {
if (args.AddIfNotPresent.StartsWith('/'))
Chat = args.AddIfNotPresent;
else
Chat += args.AddIfNotPresent;
}
if (args.Input != null) if (args.Input != null)
Chat += args.Input; {
if (args.Input.StartsWith('/'))
Chat = args.Input;
else
Chat += args.Input;
}
var (info, reason, target) = (args.ChannelSwitchInfo, args.TellReason, args.TellTarget); var (info, reason, target) = (args.ChannelSwitchInfo, args.TellReason, args.TellTarget);
@@ -280,7 +309,7 @@ public sealed class ChatLogWindow : Window
|| !GameFunctions.Chat.IsChannelOrExistingLinkshell(targetChannel.Value) || !GameFunctions.Chat.IsChannelOrExistingLinkshell(targetChannel.Value)
) )
{ {
Plugin.LogProxy.Warning( _logger.LogWarning(
$"Channel was set to an invalid value '{targetChannel}', ignoring" $"Channel was set to an invalid value '{targetChannel}', ignoring"
); );
return; return;
@@ -334,11 +363,11 @@ public sealed class ChatLogWindow : Window
{ {
case "hide": case "hide":
CurrentHideState = HideState.User; CurrentHideState = HideState.User;
Plugin.LogProxy.Verbose("HideState: → User (chat hide command)"); _logger.LogTrace("HideState: → User (chat hide command)");
break; break;
case "show": case "show":
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
Plugin.LogProxy.Verbose("HideState: → None (chat show command)"); _logger.LogTrace("HideState: → None (chat show command)");
break; break;
case "toggle": case "toggle":
CurrentHideState = CurrentHideState switch CurrentHideState = CurrentHideState switch
@@ -348,7 +377,7 @@ public sealed class ChatLogWindow : Window
HideState.None => HideState.User, HideState.None => HideState.User,
_ => CurrentHideState, _ => CurrentHideState,
}; };
Plugin.LogProxy.Verbose($"HideState: → {CurrentHideState} (chat toggle command)"); _logger.LogTrace($"HideState: → {CurrentHideState} (chat toggle command)");
break; break;
} }
} }
@@ -458,7 +487,7 @@ public sealed class ChatLogWindow : Window
else if (newTab.CurrentChannel.Channel is InputChannel.Invalid) else if (newTab.CurrentChannel.Channel is InputChannel.Invalid)
{ {
newTab.CurrentChannel = previousTab.CurrentChannel.Clone(); newTab.CurrentChannel = previousTab.CurrentChannel.Clone();
Plugin.LogProxy.Debug( _logger.LogDebug(
$"[Tab] '{newTab.Name}' seeded channel from '{previousTab.Name}' " $"[Tab] '{newTab.Name}' seeded channel from '{previousTab.Name}' "
+ $"(Channel={newTab.CurrentChannel.Channel}, TellTarget={newTab.CurrentChannel.TellTarget?.ToTargetString() ?? "null"})" + $"(Channel={newTab.CurrentChannel.Channel}, TellTarget={newTab.CurrentChannel.TellTarget?.ToTargetString() ?? "null"})"
); );
@@ -486,14 +515,14 @@ public sealed class ChatLogWindow : Window
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.LogProxy.Verbose("HideState: None → Battle"); _logger.LogTrace("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.LogProxy.Verbose("HideState: Battle → None"); _logger.LogTrace("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
@@ -506,7 +535,7 @@ public sealed class ChatLogWindow : Window
if (Plugin.Functions.Chat.CheckHideFlags()) if (Plugin.Functions.Chat.CheckHideFlags())
{ {
CurrentHideState = HideState.Cutscene; CurrentHideState = HideState.Cutscene;
Plugin.LogProxy.Verbose("HideState: None → Cutscene"); _logger.LogTrace("HideState: None → Cutscene");
} }
} }
@@ -517,7 +546,7 @@ public sealed class ChatLogWindow : Window
&& !Plugin.GposeActive && !Plugin.GposeActive
) )
{ {
Plugin.LogProxy.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)"); _logger.LogTrace($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
} }
@@ -525,14 +554,14 @@ public sealed class ChatLogWindow : Window
if (CurrentHideState == HideState.Cutscene && Activate) if (CurrentHideState == HideState.Cutscene && Activate)
{ {
CurrentHideState = HideState.CutsceneOverride; CurrentHideState = HideState.CutsceneOverride;
Plugin.LogProxy.Verbose("HideState: Cutscene → CutsceneOverride (user activate)"); _logger.LogTrace("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.LogProxy.Verbose("HideState: User → None (activate)"); _logger.LogTrace("HideState: User → None (activate)");
} }
if ( if (
@@ -663,7 +692,7 @@ public sealed class ChatLogWindow : Window
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Error drawing Chat Log window"); _logger.LogError(ex, "Error drawing Chat Log window");
if (!NotifiedDrawFailure) if (!NotifiedDrawFailure)
{ {
Plugin.Notification.AddNotification( Plugin.Notification.AddNotification(
@@ -1705,7 +1734,7 @@ public sealed class ChatLogWindow : Window
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Warning(ex, "Error drawing chat log"); _logger.LogWarning(ex, "Error drawing chat log");
} }
} }
@@ -2253,7 +2282,7 @@ public sealed class ChatLogWindow : Window
{ {
Plugin.Config.SeenPopOutHeaderHint = true; Plugin.Config.SeenPopOutHeaderHint = true;
Plugin.SaveConfig(); Plugin.SaveConfig();
Plugin.LogProxy.Debug("v0.6.1 pop-out header hint dismissed"); _logger.LogDebug("v0.6.1 pop-out header hint dismissed");
if (openSettings) if (openSettings)
Plugin.SettingsWindow.Toggle(); Plugin.SettingsWindow.Toggle();
} }
@@ -2391,7 +2420,7 @@ public sealed class ChatLogWindow : Window
if (PopOutWindows.Contains(tab.Identifier)) if (PopOutWindows.Contains(tab.Identifier))
continue; continue;
var window = new Popout(this, tab, i); var window = new Popout(this, tab, i, _loggerFactory.CreateLogger<Popout>());
Plugin.WindowSystem.AddWindow(window); Plugin.WindowSystem.AddWindow(window);
PopOutWindows.Add(tab.Identifier); PopOutWindows.Add(tab.Identifier);
@@ -2908,7 +2937,7 @@ public sealed class ChatLogWindow : Window
var viewport = ImGui.GetMainViewport(); var viewport = ImGui.GetMainViewport();
var safePos = viewport.WorkPos + SafeDefaultOffset; var safePos = viewport.WorkPos + SafeDefaultOffset;
Position = safePos; Position = safePos;
Plugin.LogProxy.Info( _logger.LogInformation(
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}." $"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."
); );
+8 -4
View File
@@ -17,6 +17,7 @@ using HellionChat.Resources;
using HellionChat.Util; using HellionChat.Util;
using Lumina.Data.Files; using Lumina.Data.Files;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using Microsoft.Extensions.Logging;
using MoreLinq; using MoreLinq;
namespace HellionChat.Ui; namespace HellionChat.Ui;
@@ -67,10 +68,13 @@ public class DbViewer : Window
private bool NeedsScrollReset; private bool NeedsScrollReset;
public DbViewer(Plugin plugin) private readonly ILogger<DbViewer> _logger;
public DbViewer(Plugin plugin, ILogger<DbViewer> logger)
: base("DBViewer###chat2-dbviewer") : base("DBViewer###chat2-dbviewer")
{ {
Plugin = plugin; Plugin = plugin;
_logger = logger;
SelectedChannels = TabsUtil.MostlyPlayer; SelectedChannels = TabsUtil.MostlyPlayer;
DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern; DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
@@ -320,7 +324,7 @@ public class DbViewer : Window
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Failed reading messages from database"); _logger.LogError(ex, "Failed reading messages from database");
} }
finally finally
{ {
@@ -483,7 +487,7 @@ public class DbViewer : Window
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "FTS filter worker failed"); _logger.LogError(ex, "FTS filter worker failed");
} }
}); });
} }
@@ -625,7 +629,7 @@ public class DbViewer : Window
} }
catch (Exception ex) catch (Exception ex)
{ {
Plugin.LogProxy.Error(ex, "Failed creating txt backup"); _logger.LogError(ex, "Failed creating txt backup");
Notification.Content = "Error ..."; Notification.Content = "Error ...";
Notification.Type = NotificationType.Error; Notification.Type = NotificationType.Error;
+11 -8
View File
@@ -3,6 +3,7 @@ using Dalamud.Bindings.ImGui;
using Dalamud.Interface.Style; using Dalamud.Interface.Style;
using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Utility.Raii;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui; namespace HellionChat.Ui;
@@ -11,6 +12,7 @@ internal class Popout : Window
private readonly ChatLogWindow ChatLogWindow; private readonly ChatLogWindow ChatLogWindow;
private readonly Tab Tab; private readonly Tab Tab;
private readonly int Idx; private readonly int Idx;
private readonly ILogger<Popout> _logger;
private long FrameTime; private long FrameTime;
private long LastActivityTime = Environment.TickCount64; private long LastActivityTime = Environment.TickCount64;
@@ -23,12 +25,13 @@ internal class Popout : Window
// Exposed so AutoTellTabsService can locate this window during LRU eviction. // Exposed so AutoTellTabsService can locate this window during LRU eviction.
internal Guid TabIdentifier => Tab.Identifier; internal Guid TabIdentifier => Tab.Identifier;
public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx) public Popout(ChatLogWindow chatLogWindow, Tab tab, int idx, ILogger<Popout> logger)
: base($"{tab.Name}##popout") : base($"{tab.Name}##popout")
{ {
ChatLogWindow = chatLogWindow; ChatLogWindow = chatLogWindow;
Tab = tab; Tab = tab;
Idx = idx; Idx = idx;
_logger = logger;
Size = new Vector2(350, 350); Size = new Vector2(350, 350);
SizeCondition = ImGuiCond.FirstUseEver; SizeCondition = ImGuiCond.FirstUseEver;
@@ -175,7 +178,7 @@ internal class Popout : Window
{ {
Plugin.Config.SeenPopOutInputHint = true; Plugin.Config.SeenPopOutInputHint = true;
ChatLogWindow.Plugin.SaveConfig(); ChatLogWindow.Plugin.SaveConfig();
Plugin.LogProxy.Debug("Pop-Out input hint dismissed"); _logger.LogDebug("Pop-Out input hint dismissed");
if (openSettings) if (openSettings)
ChatLogWindow.Plugin.SettingsWindow.Toggle(); ChatLogWindow.Plugin.SettingsWindow.Toggle();
} }
@@ -214,13 +217,13 @@ internal class Popout : Window
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle) if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
{ {
CurrentHideState = HideState.Battle; CurrentHideState = HideState.Battle;
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: None -> Battle"); _logger.LogTrace($"Popout HideState [{Tab.Name}]: None -> Battle");
} }
if (CurrentHideState is HideState.Battle && !Plugin.InBattle) if (CurrentHideState is HideState.Battle && !Plugin.InBattle)
{ {
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: Battle -> None"); _logger.LogTrace($"Popout HideState [{Tab.Name}]: Battle -> None");
} }
if ( if (
@@ -232,7 +235,7 @@ internal class Popout : Window
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags()) if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
{ {
CurrentHideState = HideState.Cutscene; CurrentHideState = HideState.Cutscene;
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: None -> Cutscene"); _logger.LogTrace($"Popout HideState [{Tab.Name}]: None -> Cutscene");
} }
} }
@@ -242,7 +245,7 @@ internal class Popout : Window
&& !Plugin.GposeActive && !Plugin.GposeActive
) )
{ {
Plugin.LogProxy.Verbose( _logger.LogTrace(
$"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)" $"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)"
); );
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
@@ -251,7 +254,7 @@ internal class Popout : Window
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate) if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
{ {
CurrentHideState = HideState.CutsceneOverride; CurrentHideState = HideState.CutsceneOverride;
Plugin.LogProxy.Verbose( _logger.LogTrace(
$"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)" $"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)"
); );
} }
@@ -259,7 +262,7 @@ internal class Popout : Window
if (CurrentHideState == HideState.User && ChatLogWindow.Activate) if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
{ {
CurrentHideState = HideState.None; CurrentHideState = HideState.None;
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: User -> None (activate)"); _logger.LogTrace($"Popout HideState [{Tab.Name}]: User -> None (activate)");
} }
return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle return CurrentHideState is HideState.Cutscene or HideState.User or HideState.Battle
+5 -4
View File
@@ -6,6 +6,7 @@ using Dalamud.Utility;
using HellionChat.Resources; using HellionChat.Resources;
using HellionChat.Ui.SettingsTabs; using HellionChat.Ui.SettingsTabs;
using HellionChat.Util; using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui; namespace HellionChat.Ui;
@@ -25,7 +26,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
private SettingsView View = SettingsView.Overview; private SettingsView View = SettingsView.Overview;
private readonly SettingsOverview Overview; private readonly SettingsOverview Overview;
internal SettingsWindow(Plugin plugin) internal SettingsWindow(Plugin plugin, ILoggerFactory loggerFactory)
: base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings") : base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
{ {
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse; Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
@@ -45,13 +46,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
Tabs = Tabs =
[ [
new General(Plugin, Mutable), new General(Plugin, Mutable),
new ThemeAndLayout(Plugin, Mutable), new ThemeAndLayout(Plugin, Mutable, loggerFactory.CreateLogger<ThemeAndLayout>()),
new FontsAndColours(Plugin, Mutable), new FontsAndColours(Plugin, Mutable, loggerFactory.CreateLogger<FontsAndColours>()),
new SettingsTabs.Window(Plugin, Mutable), new SettingsTabs.Window(Plugin, Mutable),
new Chat(Plugin, Mutable), new Chat(Plugin, Mutable),
new SettingsTabs.Tabs(Plugin, Mutable), new SettingsTabs.Tabs(Plugin, Mutable),
new SettingsTabs.Privacy(Plugin, Mutable), new SettingsTabs.Privacy(Plugin, Mutable),
new DataManagement(Plugin, Mutable), new DataManagement(Plugin, Mutable, loggerFactory.CreateLogger<DataManagement>()),
new SettingsTabs.Integrations(Plugin, Mutable), new SettingsTabs.Integrations(Plugin, Mutable),
new Information(Mutable), new Information(Mutable),
]; ];
+19 -20
View File
@@ -11,6 +11,7 @@ using HellionChat.Export;
using HellionChat.Privacy; using HellionChat.Privacy;
using HellionChat.Resources; using HellionChat.Resources;
using HellionChat.Util; using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
@@ -18,6 +19,7 @@ internal sealed class DataManagement : ISettingsTab
{ {
private Plugin Plugin { get; } private Plugin Plugin { get; }
private Configuration Mutable { get; } private Configuration Mutable { get; }
private readonly ILogger<DataManagement> _logger;
public string Name => public string Name =>
HellionStrings.Settings_Card_DataManagement_Title + "###tabs-datamanagement"; HellionStrings.Settings_Card_DataManagement_Title + "###tabs-datamanagement";
@@ -136,10 +138,11 @@ internal sealed class DataManagement : ISettingsTab
), ),
]; ];
internal DataManagement(Plugin plugin, Configuration mutable) internal DataManagement(Plugin plugin, Configuration mutable, ILogger<DataManagement> logger)
{ {
Plugin = plugin; Plugin = plugin;
Mutable = mutable; Mutable = mutable;
_logger = logger;
} }
public void Draw(bool changed) public void Draw(bool changed)
@@ -229,7 +232,7 @@ internal sealed class DataManagement : ISettingsTab
} }
catch (Exception e) catch (Exception e)
{ {
Plugin.LogProxy.Error(e, "Unable to delete old database"); _logger.LogError(e, "Unable to delete old database");
WrapperUtil.AddNotification( WrapperUtil.AddNotification(
Language.Options_Database_Old_Delete_Error, Language.Options_Database_Old_Delete_Error,
NotificationType.Error NotificationType.Error
@@ -391,9 +394,7 @@ internal sealed class DataManagement : ISettingsTab
Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow; Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
Plugin.SaveConfig(); Plugin.SaveConfig();
Plugin.LogProxy.Information( _logger.LogInformation($"Manual retention run deleted {deleted} expired messages.");
$"Manual retention run deleted {deleted} expired messages."
);
if (deleted > 0) if (deleted > 0)
{ {
@@ -407,7 +408,7 @@ internal sealed class DataManagement : ISettingsTab
.Wait(TimeSpan.FromSeconds(5)) .Wait(TimeSpan.FromSeconds(5))
) )
{ {
Plugin.LogProxy.Warning( _logger.LogWarning(
"Retention sweep: framework refresh timed out after 5s." "Retention sweep: framework refresh timed out after 5s."
); );
} }
@@ -420,7 +421,7 @@ internal sealed class DataManagement : ISettingsTab
} }
catch (Exception e) catch (Exception e)
{ {
Plugin.LogProxy.Error(e, "Manual retention run failed"); _logger.LogError(e, "Manual retention run failed");
WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error); WrapperUtil.AddNotification(HellionStrings.Retention_Error, NotificationType.Error);
} }
finally finally
@@ -568,7 +569,7 @@ internal sealed class DataManagement : ISettingsTab
} }
catch (Exception e) catch (Exception e)
{ {
Plugin.LogProxy.Error(e, "Failed to compute cleanup preview"); _logger.LogError(e, "Failed to compute cleanup preview");
WrapperUtil.AddNotification( WrapperUtil.AddNotification(
HellionStrings.Cleanup_PreviewError, HellionStrings.Cleanup_PreviewError,
NotificationType.Error NotificationType.Error
@@ -589,7 +590,7 @@ internal sealed class DataManagement : ISettingsTab
try try
{ {
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed); var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
Plugin.LogProxy.Information($"Privacy cleanup: deleted {deleted} messages"); _logger.LogInformation($"Privacy cleanup: deleted {deleted} messages");
if ( if (
!Plugin !Plugin
@@ -601,9 +602,7 @@ internal sealed class DataManagement : ISettingsTab
.Wait(TimeSpan.FromSeconds(5)) .Wait(TimeSpan.FromSeconds(5))
) )
{ {
Plugin.LogProxy.Warning( _logger.LogWarning("Privacy cleanup: framework refresh timed out after 5s.");
"Privacy cleanup: framework refresh timed out after 5s."
);
} }
WrapperUtil.AddNotification( WrapperUtil.AddNotification(
@@ -613,7 +612,7 @@ internal sealed class DataManagement : ISettingsTab
} }
catch (Exception e) catch (Exception e)
{ {
Plugin.LogProxy.Error(e, "Privacy cleanup failed"); _logger.LogError(e, "Privacy cleanup failed");
WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error); WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error);
} }
finally finally
@@ -773,7 +772,7 @@ internal sealed class DataManagement : ISettingsTab
} }
catch (Exception e) catch (Exception e)
{ {
Plugin.LogProxy.Error(e, "Export failed"); _logger.LogError(e, "Export failed");
WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error); WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error);
} }
finally finally
@@ -853,7 +852,7 @@ internal sealed class DataManagement : ISettingsTab
) )
) )
{ {
Plugin.LogProxy.Warning("Clearing messages from database"); _logger.LogWarning("Clearing messages from database");
Plugin.MessageManager.Store.ClearMessages(); Plugin.MessageManager.Store.ClearMessages();
Plugin.MessageManager.ClearAllTabs(); Plugin.MessageManager.ClearAllTabs();
@@ -911,7 +910,7 @@ internal sealed class DataManagement : ISettingsTab
private void InsertMessages(int count) private void InsertMessages(int count)
{ {
Plugin.LogProxy.Info($"Inserting {count} messages due to user request"); _logger.LogInformation($"Inserting {count} messages due to user request");
var stopwatch = Stopwatch.StartNew(); var stopwatch = Stopwatch.StartNew();
var playerName = Plugin.PlayerState.CharacterName; var playerName = Plugin.PlayerState.CharacterName;
@@ -956,7 +955,7 @@ internal sealed class DataManagement : ISettingsTab
var elapsedTicks = stopwatch.ElapsedTicks; var elapsedTicks = stopwatch.ElapsedTicks;
stopwatch.Stop(); stopwatch.Stop();
Plugin.LogProxy.Info( _logger.LogInformation(
$"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)" $"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
); );
@@ -966,7 +965,7 @@ internal sealed class DataManagement : ISettingsTab
elapsedTicks = stopwatch.ElapsedTicks; elapsedTicks = stopwatch.ElapsedTicks;
stopwatch.Stop(); stopwatch.Stop();
Plugin.LogProxy.Info( _logger.LogInformation(
$"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)" $"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
); );
@@ -977,7 +976,7 @@ internal sealed class DataManagement : ISettingsTab
Plugin.MessageManager.ClearAllTabs(); Plugin.MessageManager.ClearAllTabs();
elapsedTicks = stopwatch.ElapsedTicks; elapsedTicks = stopwatch.ElapsedTicks;
stopwatch.Stop(); stopwatch.Stop();
Plugin.LogProxy.Info( _logger.LogInformation(
$"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)" $"Cleared {Plugin.Config.Tabs.Count} tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
); );
}) })
@@ -990,7 +989,7 @@ internal sealed class DataManagement : ISettingsTab
Plugin.MessageManager.FilterAllTabs(); Plugin.MessageManager.FilterAllTabs();
elapsedTicks = stopwatch.ElapsedTicks; elapsedTicks = stopwatch.ElapsedTicks;
stopwatch.Stop(); stopwatch.Stop();
Plugin.LogProxy.Info( _logger.LogInformation(
$"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)" $"Fetched and filtered all tabs in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
); );
}) })
@@ -7,6 +7,7 @@ using Dalamud.Interface.Utility.Raii;
using HellionChat.Code; using HellionChat.Code;
using HellionChat.Resources; using HellionChat.Resources;
using HellionChat.Util; using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
@@ -14,14 +15,16 @@ internal sealed class FontsAndColours : ISettingsTab
{ {
private Plugin Plugin { get; } private Plugin Plugin { get; }
private Configuration Mutable { get; } private Configuration Mutable { get; }
private readonly ILogger<FontsAndColours> _logger;
public string Name => public string Name =>
HellionStrings.Settings_Card_FontsAndColours_Title + "###tabs-fontsandcolours"; HellionStrings.Settings_Card_FontsAndColours_Title + "###tabs-fontsandcolours";
internal FontsAndColours(Plugin plugin, Configuration mutable) internal FontsAndColours(Plugin plugin, Configuration mutable, ILogger<FontsAndColours> logger)
{ {
Plugin = plugin; Plugin = plugin;
Mutable = mutable; Mutable = mutable;
_logger = logger;
} }
public void Draw(bool changed) public void Draw(bool changed)
@@ -312,6 +315,6 @@ internal sealed class FontsAndColours : ISettingsTab
} }
Plugin.SaveConfig(); Plugin.SaveConfig();
GlobalParametersCache.Refresh(); GlobalParametersCache.Refresh();
Plugin.LogProxy.Debug($"Applied chat colour preset: {preset.DisplayName}"); _logger.LogDebug($"Applied chat colour preset: {preset.DisplayName}");
} }
} }
@@ -4,6 +4,7 @@ using Dalamud.Interface.Utility.Raii;
using HellionChat.Resources; using HellionChat.Resources;
using HellionChat.Themes; using HellionChat.Themes;
using HellionChat.Util; using HellionChat.Util;
using Microsoft.Extensions.Logging;
namespace HellionChat.Ui.SettingsTabs; namespace HellionChat.Ui.SettingsTabs;
@@ -11,16 +12,18 @@ internal sealed class ThemeAndLayout : ISettingsTab
{ {
private Plugin Plugin { get; } private Plugin Plugin { get; }
private Configuration Mutable { get; } private Configuration Mutable { get; }
private readonly ILogger<ThemeAndLayout> _logger;
private string? _applyDismissedFor; private string? _applyDismissedFor;
public string Name => public string Name =>
HellionStrings.Settings_Card_ThemeAndLayout_Title + "###tabs-themeandlayout"; HellionStrings.Settings_Card_ThemeAndLayout_Title + "###tabs-themeandlayout";
internal ThemeAndLayout(Plugin plugin, Configuration mutable) internal ThemeAndLayout(Plugin plugin, Configuration mutable, ILogger<ThemeAndLayout> logger)
{ {
Plugin = plugin; Plugin = plugin;
Mutable = mutable; Mutable = mutable;
_logger = logger;
} }
public void Draw(bool changed) public void Draw(bool changed)
@@ -90,7 +93,7 @@ internal sealed class ThemeAndLayout : ISettingsTab
var path = Path.Combine(dir, fileName); var path = Path.Combine(dir, fileName);
var json = ThemeJsonWriter.Serialize(active); var json = ThemeJsonWriter.Serialize(active);
File.WriteAllText(path, json); File.WriteAllText(path, json);
Plugin.LogProxy.Information($"Exported active theme '{active.Slug}' to {path}"); _logger.LogInformation($"Exported active theme '{active.Slug}' to {path}");
} }
} }
} }
+4 -4
View File
@@ -2,10 +2,10 @@ using System;
namespace HellionChat.Util; namespace HellionChat.Util;
// Indirection over Dalamud's IPluginLog so MessageStore can be constructed // Plugin.LogProxy bridge for consumers that cannot take a logger via the
// in an isolated xUnit AppDomain without loading Dalamud.dll — same pattern // constructor: static helpers (EmoteCache et al.), Dalamud-reflected types
// as IPlatformUtil from F12.1. A later DI-container cycle (v1.5.x) may // (Configuration), data classes with mass instantiation (Message) and
// replace this with Microsoft.Extensions.Logging's ILogger<T>. // instance classes that only log from static methods (FontManager).
internal interface IPluginLogProxy internal interface IPluginLogProxy
{ {
void Verbose(string message); void Verbose(string message);
+395 -107
View File
@@ -1,110 +1,398 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net10.0-windows7.0": { "net10.0-windows7.0": {
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[15.0.0, )", "requested": "[15.0.0, )",
"resolved": "15.0.0", "resolved": "15.0.0",
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ==" "contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
}, },
"DotNet.ReproducibleBuilds": { "DotNet.ReproducibleBuilds": {
"type": "Direct", "type": "Direct",
"requested": "[1.2.39, )", "requested": "[1.2.39, )",
"resolved": "1.2.39", "resolved": "1.2.39",
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg==" "contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
}, },
"MessagePack": { "MessagePack": {
"type": "Direct", "type": "Direct",
"requested": "[3.1.4, 4.0.0)", "requested": "[3.1.4, 4.0.0)",
"resolved": "3.1.4", "resolved": "3.1.4",
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==", "contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
"dependencies": { "dependencies": {
"MessagePack.Annotations": "3.1.4", "MessagePack.Annotations": "3.1.4",
"MessagePackAnalyzer": "3.1.4", "MessagePackAnalyzer": "3.1.4",
"Microsoft.NET.StringTools": "17.11.4" "Microsoft.NET.StringTools": "17.11.4"
}
},
"Microsoft.Data.Sqlite": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "DZ6G2QuyPrsh5VS+wfiZbNBtYT6p+CkxXjD0aZHF04xso7QsG/uk0JpG30hzYlK6u/wtTzta1Dqfgbc/Sl2sDA==",
"dependencies": {
"Microsoft.Data.Sqlite.Core": "10.0.7",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
"SQLitePCLRaw.core": "2.1.11"
}
},
"morelinq": {
"type": "Direct",
"requested": "[4.4.0, )",
"resolved": "4.4.0",
"contentHash": "QX3bsK9oFeUXk8tFsc9NkI6NnCr8Ar/ex027p+ZZ/jdLCdX2RlryDtxUqZW5j45NVwn4E4Z4hzupsoMQd6Yxtg=="
},
"Pidgin": {
"type": "Direct",
"requested": "[3.5.1, 4.0.0)",
"resolved": "3.5.1",
"contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g=="
},
"SixLabors.ImageSharp": {
"type": "Direct",
"requested": "[3.1.12, 4.0.0)",
"resolved": "3.1.12",
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
},
"SQLitePCLRaw.lib.e_sqlite3": {
"type": "Direct",
"requested": "[3.50.3, )",
"resolved": "3.50.3",
"contentHash": "tVyhqQ8wxgedWiiPFChyZhE8I3PkOM/AE1azsj1qsdYUws13ONBFyi3aDxju4tD2kzedB2q5+50WrTyY0h2gMQ=="
},
"MessagePack.Annotations": {
"type": "Transitive",
"resolved": "3.1.4",
"contentHash": "aVWrDAkCdqxwQsz/q0ldPh2EFn48M99YUzE9OvZjMq2RNLKz4o2z88iGFvSvbMqOWRweRvKPHBJZe22PRqzslQ=="
},
"MessagePackAnalyzer": {
"type": "Transitive",
"resolved": "3.1.4",
"contentHash": "CTaSsN/liJ7MhLCAB7Z4ZLBNuVGCq9lt2BT/cbrc9vzGv89yK3CqIA+z9T19a11eQYl9etZHL6MQJgCqECRVpg=="
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "xVrtBg3M1wJlBDkoT0dXEYB/wSc8bIHJPYtw/bu1AqpWgF79uPSs87DAhERR/Ilumre6TKZa1cjMg3VUUObVLA==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "17.11.4",
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
},
"SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
"dependencies": {
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
"SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
}
},
"SQLitePCLRaw.core": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA=="
},
"SQLitePCLRaw.provider.e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.11"
}
}
} }
},
"Microsoft.Data.Sqlite": {
"type": "Direct",
"requested": "[10.0.7, )",
"resolved": "10.0.7",
"contentHash": "DZ6G2QuyPrsh5VS+wfiZbNBtYT6p+CkxXjD0aZHF04xso7QsG/uk0JpG30hzYlK6u/wtTzta1Dqfgbc/Sl2sDA==",
"dependencies": {
"Microsoft.Data.Sqlite.Core": "10.0.7",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.11",
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Direct",
"requested": "[10.0.7, 11.0.0)",
"resolved": "10.0.7",
"contentHash": "91F/o3emPV/+xY/ip3s2LqDNF14kjttlVtq0BXgg6p4MnCzeSZxnUJm+t6WRrtD3JdGo88/oX+z7OwK4y8PZuw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Hosting": {
"type": "Direct",
"requested": "[10.0.7, 11.0.0)",
"resolved": "10.0.7",
"contentHash": "M/vBpfWcschvS2EUeq7cHfscsxabiGTptXwV7GeSueovGiSoNjyo1j5PMcWuOAAQrRW3nRqxZk8NeumrmpzUBg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.7",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
"Microsoft.Extensions.Configuration.Binder": "10.0.7",
"Microsoft.Extensions.Configuration.CommandLine": "10.0.7",
"Microsoft.Extensions.Configuration.EnvironmentVariables": "10.0.7",
"Microsoft.Extensions.Configuration.FileExtensions": "10.0.7",
"Microsoft.Extensions.Configuration.Json": "10.0.7",
"Microsoft.Extensions.Configuration.UserSecrets": "10.0.7",
"Microsoft.Extensions.DependencyInjection": "10.0.7",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Diagnostics": "10.0.7",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
"Microsoft.Extensions.FileProviders.Physical": "10.0.7",
"Microsoft.Extensions.Hosting.Abstractions": "10.0.7",
"Microsoft.Extensions.Logging": "10.0.7",
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
"Microsoft.Extensions.Logging.Configuration": "10.0.7",
"Microsoft.Extensions.Logging.Console": "10.0.7",
"Microsoft.Extensions.Logging.Debug": "10.0.7",
"Microsoft.Extensions.Logging.EventLog": "10.0.7",
"Microsoft.Extensions.Logging.EventSource": "10.0.7",
"Microsoft.Extensions.Options": "10.0.7"
}
},
"Microsoft.Extensions.Logging": {
"type": "Direct",
"requested": "[10.0.7, 11.0.0)",
"resolved": "10.0.7",
"contentHash": "hOeRIQ63GkgiYCB/MIFp+LQs8aXpJXpB55t6Aj37ab7t2/6WeFcPXxYM9hdy/o5tffzwf8mhqzLJP6mjGYCxjw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "10.0.7",
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
"Microsoft.Extensions.Options": "10.0.7"
}
},
"Microsoft.Extensions.Options": {
"type": "Direct",
"requested": "[10.0.7, 11.0.0)",
"resolved": "10.0.7",
"contentHash": "00SHUGTh2jSMvIr6x9Xwd2nE+B5/qFCO/9hDwUDhJsjYRDlADmaBZ7tqehXzBDsfjHSXJzuRHJzPYPPjphBQ7Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Primitives": "10.0.7"
}
},
"morelinq": {
"type": "Direct",
"requested": "[4.4.0, )",
"resolved": "4.4.0",
"contentHash": "QX3bsK9oFeUXk8tFsc9NkI6NnCr8Ar/ex027p+ZZ/jdLCdX2RlryDtxUqZW5j45NVwn4E4Z4hzupsoMQd6Yxtg=="
},
"Pidgin": {
"type": "Direct",
"requested": "[3.5.1, 4.0.0)",
"resolved": "3.5.1",
"contentHash": "zU7tkXlF3D6d2GLTjJDomAL3nnl4AwfZvSgSz8D4b+Ry21/clqedYlxBnEAkAU/bkGfEv6uRR7QCdWZUpKrB/g=="
},
"SixLabors.ImageSharp": {
"type": "Direct",
"requested": "[3.1.12, 4.0.0)",
"resolved": "3.1.12",
"contentHash": "iAg6zifihXEFS/t7fiHhZBGAdCp3FavsF4i2ZIDp0JfeYeDVzvmlbY1CNhhIKimaIzrzSi5M/NBFcWvZT2rB/A=="
},
"SQLitePCLRaw.lib.e_sqlite3": {
"type": "Direct",
"requested": "[3.50.3, )",
"resolved": "3.50.3",
"contentHash": "tVyhqQ8wxgedWiiPFChyZhE8I3PkOM/AE1azsj1qsdYUws13ONBFyi3aDxju4tD2kzedB2q5+50WrTyY0h2gMQ=="
},
"MessagePack.Annotations": {
"type": "Transitive",
"resolved": "3.1.4",
"contentHash": "aVWrDAkCdqxwQsz/q0ldPh2EFn48M99YUzE9OvZjMq2RNLKz4o2z88iGFvSvbMqOWRweRvKPHBJZe22PRqzslQ=="
},
"MessagePackAnalyzer": {
"type": "Transitive",
"resolved": "3.1.4",
"contentHash": "CTaSsN/liJ7MhLCAB7Z4ZLBNuVGCq9lt2BT/cbrc9vzGv89yK3CqIA+z9T19a11eQYl9etZHL6MQJgCqECRVpg=="
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "xVrtBg3M1wJlBDkoT0dXEYB/wSc8bIHJPYtw/bu1AqpWgF79uPSs87DAhERR/Ilumre6TKZa1cjMg3VUUObVLA==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.11"
}
},
"Microsoft.Extensions.Configuration": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "wZbGh7J8R1vXN525O6d8dlcDTxhRTnd5MyW4LdfP5S0tSnTwTCseYSrq6g0Mxh7W9xn8P/2xPuf0D/m6k2dy2w==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
"Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Microsoft.Extensions.Configuration.Abstractions": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "t56nEgvECcyLPojZIUFWJknQQDAbgfTf9J+QMYJE1YYvVgz69vN6B/AKL8Grvj3Lcnp8kTpNqwmwFhb3YLJmtQ==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Microsoft.Extensions.Configuration.Binder": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "8bS1qIaRivny+WX+49pmeJ6iAylbtX8C0DLEcCQWZjdxQvLqaMssXiGD9P/6pYElrHbK5/nAHmjbQ8STqdMYeg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.7",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Configuration.CommandLine": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "3lNjglxfFxOzI9zG+3HSg/YSGqo//8Fqw6u6iuIamZb4JCorbA3JLaeWOpfKTAPi2UJwaispOXWx14dUqcGz4A==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.7",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Configuration.EnvironmentVariables": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "TWto3imA+mJMLZI+5sbgLiFFoOFNFkizQYNaC5jTuiHKn3diwm1RN7mWDOEZN9kG2bixw7IvgpvtUG5/teSRzA==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.7",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Configuration.FileExtensions": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "qbZLvLsoTdArSloEnSxs21P781YUmwVmHc5NJPQD/ezAreQ7884z+6QfAZVKi86WAZtzx83jK2uC4itxOM44gQ==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.7",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
"Microsoft.Extensions.FileProviders.Physical": "10.0.7",
"Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Microsoft.Extensions.Configuration.Json": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "64dimvyyKk0dbUbrLg/YCv4ugJ4sVz2aXLwfvZwR1EC4tJqW9ru/oVRcXwoJRa2lQGXtYtlpk4maWOeIb48tQw==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.7",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
"Microsoft.Extensions.Configuration.FileExtensions": "10.0.7",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Configuration.UserSecrets": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "YqVIICoIdl0016wkeO2WQS+uEbEXbUhMLKdC5rZNl1X3nu59F+nwaAHdHjq/4OK+Cx31DYmNUSFh+MUot8qSDw==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
"Microsoft.Extensions.Configuration.Json": "10.0.7",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
"Microsoft.Extensions.FileProviders.Physical": "10.0.7"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "Z6mfFEaFcwCfSboxJwOLfu7/31npCY9q70WUamHW/vRQhDvBKOT4Vf9YkZj5J6hLvJpb0oDEYfHunQZj0xxvKw=="
},
"Microsoft.Extensions.Diagnostics": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "l+smp1qPlU0OUXD0OGfdp7OUFrbdq7ZaP5T7m2WpfZ4RFKD7iG73BAT7tjSMxNmbSXkhAn1jYHOAqzYG1r9sNg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.7",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.7",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.7"
}
},
"Microsoft.Extensions.Diagnostics.Abstractions": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "uJ9JP677y+uy+C0vtaSfi7XXgFAdz8DhU3M9lwwIXDfQKcyQ0yxM9DVYa0NXDtdVTYA2eBUtVFZ8LY0GCdeE/w==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Options": "10.0.7"
}
},
"Microsoft.Extensions.FileProviders.Abstractions": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "teioDgVpi8L186wUfrXQV1YuBt6lCSPmFZiMZo53+FZxHFjOV+f4GXo4LXgJ273Mku9//AdXWVjk9J7eJP6inw==",
"dependencies": {
"Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Microsoft.Extensions.FileProviders.Physical": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "zhgWg/i0ECj5v0jLFBSZHplvc5ygCI91DR4nne+BP4XAKF5ycz0pEKnFiTw8C1jCABJEZsnBZh6pXAvn71kFmw==",
"dependencies": {
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
"Microsoft.Extensions.FileSystemGlobbing": "10.0.7",
"Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Microsoft.Extensions.FileSystemGlobbing": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "NTUspqB+vH9g4wAD6KPOBx01xqYuKXR/cHXm449zpbq1GqfjdAxBmg7eJXrNsPw7SKwIdT2cJ05GxYVvc+lvsA=="
},
"Microsoft.Extensions.Hosting.Abstractions": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "5s8d6qC6EA8UOI4wR/+zlsq7SXttJMRb9d7zvVZ7+bE3CQEfVtC9ITUDCommm87R1zzj6WJBbCnztuIJXnP3DA==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Diagnostics.Abstractions": "10.0.7",
"Microsoft.Extensions.FileProviders.Abstractions": "10.0.7",
"Microsoft.Extensions.Logging.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "tIEcQ2gvERrH2KiCjdsVcHGhXt9lIsuDStfOIeZWr7/fP8IXhGiYfx0/80PNI7WPO2IYuFtlZLSlnTS8+/Mchw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Logging.Configuration": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "7BBnoGF37USiu7j434put9mDp7EjdlNDIZsR4vHfC1FbLZeLqiWjgJbeEtF0p59Ryqt8AtraHawf0ZKbe5jibg==",
"dependencies": {
"Microsoft.Extensions.Configuration": "10.0.7",
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
"Microsoft.Extensions.Configuration.Binder": "10.0.7",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Logging": "10.0.7",
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
"Microsoft.Extensions.Options": "10.0.7",
"Microsoft.Extensions.Options.ConfigurationExtensions": "10.0.7"
}
},
"Microsoft.Extensions.Logging.Console": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "DA++Es6v6W0HfrOrw+K8WyN6jNnZHp640PDdEvl8yfeVmgflKdn6vSSFvufNUSOuY+M2ZaSUgfY+jUKtNpXcCw==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Logging": "10.0.7",
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
"Microsoft.Extensions.Logging.Configuration": "10.0.7",
"Microsoft.Extensions.Options": "10.0.7"
}
},
"Microsoft.Extensions.Logging.Debug": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "Y6DSt/JZApunYWKqTtqbdsR6iqAvHx3D0tavbNJ1rnC24MUpF+3XO/VKgFi+9PFqMyvQ2GHBBGb8H3cLSw7rDg==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Logging": "10.0.7",
"Microsoft.Extensions.Logging.Abstractions": "10.0.7"
}
},
"Microsoft.Extensions.Logging.EventLog": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "1C8eTuxF6BLncNSJ1HCfmaBcjpUSqQDPlBVdYTlet9oldHTPpNh9iatxSJLs8TOqdp/FOpH+nSLdBve7fu9mTQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Logging": "10.0.7",
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
"Microsoft.Extensions.Options": "10.0.7",
"System.Diagnostics.EventLog": "10.0.7"
}
},
"Microsoft.Extensions.Logging.EventSource": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "YWfndnDX1jVMGCN8d5T+rO+BO8sDw6BkYlUk0BYui+WP7+HhlWx8QLdA4yUDjrkGVb3AQxIWWEPVKw5Nnfj5GQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Logging": "10.0.7",
"Microsoft.Extensions.Logging.Abstractions": "10.0.7",
"Microsoft.Extensions.Options": "10.0.7",
"Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Microsoft.Extensions.Options.ConfigurationExtensions": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "IT7f+EMXZtkjatEcF+o6aOw/7OE4etRrMiDGEWH/iiTu2R3uhC4NEQJCfHiibtX45U3sIQ5Fh6tbb1qaOz3YAg==",
"dependencies": {
"Microsoft.Extensions.Configuration.Abstractions": "10.0.7",
"Microsoft.Extensions.Configuration.Binder": "10.0.7",
"Microsoft.Extensions.DependencyInjection.Abstractions": "10.0.7",
"Microsoft.Extensions.Options": "10.0.7",
"Microsoft.Extensions.Primitives": "10.0.7"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "D5M0Jr551iTgwkZMN9rm0pSkgNLj5quUWQUmQPMZh7k/bnvZTnXRGfE2KuvXf1EEjt/ofD9yw9IumpgdP9QCnw=="
},
"Microsoft.NET.StringTools": {
"type": "Transitive",
"resolved": "17.11.4",
"contentHash": "mudqUHhNpeqIdJoUx2YDWZO/I9uEDYVowan89R6wsomfnUJQk6HteoQTlNjZDixhT2B4IXMkMtgZtoceIjLRmA=="
},
"SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "DC4nA7yWnf4UZdgJDF+9Mus4/cb0Y3Sfgi3gDnAoKNAIBwzkskNAbNbyu+u4atT0ruVlZNJfwZmwiEwE5oz9LQ==",
"dependencies": {
"SQLitePCLRaw.lib.e_sqlite3": "2.1.11",
"SQLitePCLRaw.provider.e_sqlite3": "2.1.11"
}
},
"SQLitePCLRaw.core": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "PK0GLFkfhZzLQeR3PJf71FmhtHox+U3vcY6ZtswoMjrefkB9k6ErNJEnwXqc5KgXDSjige2XXrezqS39gkpQKA=="
},
"SQLitePCLRaw.provider.e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.11",
"contentHash": "Y/0ZkR+r0Cg3DQFuCl1RBnv/tmxpIZRU3HUvelPw6MVaKHwYYR8YNvgs0vuNuXCMvlyJ+Fh88U1D4tah1tt6qw==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.11"
}
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "10.0.7",
"contentHash": "WbmDLeTPYhEzXhvYVioTVn/D1XX6bovyny9n5p8Zxtf03+eY385RB818teZm6n+fA63iZNvng0/Np4tLuhkMhQ=="
}
} }
} }
}
+15 -19
View File
@@ -2,7 +2,7 @@
[![Build](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml/badge.svg?branch=main)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml) [![Build](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml/badge.svg?branch=main)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
[![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](LICENSE) [![License: EUPL-1.2](https://img.shields.io/badge/License-EUPL--1.2-blue.svg)](LICENSE)
[![Latest release](https://img.shields.io/badge/release-v1.4.10-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest) [![Latest release](https://img.shields.io/badge/release-v1.5.0-brightgreen)](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
[![Dalamud API](https://img.shields.io/badge/Dalamud-API_15-purple)](https://github.com/goatcorp/Dalamud) [![Dalamud API](https://img.shields.io/badge/Dalamud-API_15-purple)](https://github.com/goatcorp/Dalamud)
[![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/) [![.NET](https://img.shields.io/badge/.NET-10.0-512BD4)](https://dotnet.microsoft.com/)
[![FFXIV](https://img.shields.io/badge/FFXIV-Dawntrail-c3a37f)](https://www.finalfantasyxiv.com/) [![FFXIV](https://img.shields.io/badge/FFXIV-Dawntrail-c3a37f)](https://www.finalfantasyxiv.com/)
@@ -11,7 +11,7 @@
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" /> <img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
</p> </p>
**Version 1.4.10** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on **Version 1.5.0** — Privacy-first chat plugin for FINAL FANTASY XIV / Dalamud, built on
[Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2). [Chat 2](https://github.com/Infiziert90/ChatTwo) (EUPL-1.2).
Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2 Hellion Chat is a privacy-first plugin built on the Chat 2 foundation. The majority of the engine comes from Chat 2
@@ -286,23 +286,19 @@ An optional submission to the Dalamud main plugin repo (in addition to the custo
## Project Status ## Project Status
**Version 1.4.10**Symbol-Picker and Tell-History Fix. Eleventh and final sub-patch of the v1.4.x polish sweep **Version 1.5.0**DI Foundation and Service Refactor. Major architecture cycle: the plugin bootstrap moves to a
series. A new symbol-picker popup hangs off a smile-icon button left of the channel indicator: tab one lists all generic-host DI container (`Microsoft.Extensions.Hosting` + `IServiceCollection`) modelled on Lightless Sync. All
161 FFXIV PUA glyphs (Dalamud's `SeIconChar` enum); tab two carries 97 server-verified BMP symbols (latin marks, 18 instance-class services migrate from a static `Plugin.LogProxy` locator to `Microsoft.Extensions.Logging.ILogger<T>`
currency, the full Greek alphabet, geometric shapes, suits, notes) — each one round-tripped through `/echo` and via constructor injection, with a custom `DalamudLogger` bridging the framework over to Dalamud's `IPluginLog`. The
`/say` in a four-round whitelist probe so the in-channel render matches what the picker shows. Click drops the proxy stays for the eight buckets ctor-injection cannot reach (static helpers like `EmoteCache`, Dalamud-reflected
glyph at the caret, multi-insert keeps the popup open, recent-used strip floats the last sixteen picks across `Configuration`, the `Message` data class, and static methods inside `FontManager` / `GameFunctions`). Plugin.cs
both tabs. Toggle in Settings → Chat → Message behaviour, default on. Mid-cycle hotfix for pinned auto-tell tabs: finishes the cycle at 1012 lines — virtually identical to the pre-cycle 1013 — because the new Phase-1 host build
PreloadHistory had a hidden 500-row SQL scan cap that overrode the user-configurable `AutoTellTabsHistoryPreload` and Plugin.X bridge wiring trade out exactly the service and window allocations that left `LoadAsync`. Cross-plugin
setting — active users with many tell partners lost the backlog of less-frequent pinned partners. The cap is baseline confirms no performance penalty vs Chat 2: HellionChat first-frame HITCH 77 ms median, Chat 2 74 ms.
removed; the `(Receiver, Date)` index keeps SQL fast, the client-side loop respects the user setting as the upper Lightless and XIVInstantMessenger sit around 7 ms by deferring their font-atlas build past `Finished loading`
bound. Slash-command teardown cleanup: `/hellion`, `/hellionView`, `/hellionDebugger` (and `#if DEBUG /hellionSeString`) that pattern is the v1.5.1 follow-up item. One user-visible fix bundled in from upstream: pasting a slash command
wrappers are cached as private fields so plugin teardown detaches the live registration instead of re-Register'ing into the chat input (Friend List "/tell" action, plugin-driven inserts) now replaces the existing input instead of
with identical args. The original Reserve-A `ImGuiListClipper` refactor for `DrawMessages` was cancelled after concatenating onto whatever the user was typing. Migration v17 stays (no schema bump).
cross-platform smoke showed the scroll rubber-band is a Wine/Linux render-pipeline quirk, not universal — Windows
users on v1.4.9 never saw it; the spike that targets the Wine path lives in a later patch. Migration v17 stays
(no schema bump). v1.4.x polish sweep wraps up here; next major cycle is v1.5.0 with the DI-container adoption
(`Microsoft.Extensions.Hosting` + `ILogger<T>`) modelled on Lightless (as of 2026-05-16).
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed: Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
+40
View File
@@ -10,6 +10,46 @@ to the release pages for details.
--- ---
## Hellion Chat 1.5.0 — DI Foundation and Service Refactor (2026-05-17)
Major architecture cycle. The plugin bootstrap moves to a generic-host DI container
(`Microsoft.Extensions.Hosting` + `IServiceCollection`) modelled on Lightless Sync. Service
logging migrates from a static `Plugin.LogProxy` locator to typed
`Microsoft.Extensions.Logging.ILogger<T>` via constructor injection, bridged over Dalamud's
`IPluginLog` by a custom `DalamudLogger` trio.
### Under the hood
- 18 instance-class services migrate to `ILogger<T>` via constructor injection across four
slices: data layer (`MessageStore`, `MessageManager`, `AutoTellTabsService`), IPC and
integrations (`HonorificService`, `IpcManager`, `TypingIpc`, `ExtraChat`, three
`GameFunctions` classes), UI window layer (`ChatLogWindow`, `DbViewer`, `Popout`, three
settings tabs), and root (`Commands`, `ThemeRegistry`, `PayloadHandler`).
- `Plugin.LogProxy` stays in place for the eight buckets ctor injection cannot reach:
static helpers (`EmoteCache`, `AutoTranslate`, `MemoryUtil`, `WrapperUtil`),
Dalamud-reflected types (`Configuration`), the `Message` data class, and instance classes
that only log from static methods (`FontManager`, one `GameFunctions` site).
- Plugin.cs finishes at 1012 lines — virtually identical to the pre-cycle 1013. The new
Phase-1 host build and `Plugin.X` bridge wiring trade out exactly the service and window
allocations that previously lived in `LoadAsync`.
- Cross-plugin baseline confirms no performance penalty against Chat 2: HellionChat
first-frame HITCH 77 ms median, Chat 2 74 ms median. Lightless and XIVInstantMessenger sit
around 7 ms by deferring their font-atlas build past `Finished loading` — that pattern is
the v1.5.1 follow-up item.
### User-visible
- Slash-command insert fix: pasting a slash command into the chat input (Friend List
"/tell" action, plugin-driven inserts from Artisan, AllaganTools etc.) now replaces the
existing input instead of concatenating onto whatever the user was typing. Cherry-picked
from ChatTwo upstream `ee7768ac` with namespace adaptation.
Migration v17 stays (no schema bump).
Based on Chat 2 1.35.3 (upstream Infiziert90/ChatTwo, EUPL-1.2).
---
## Hellion Chat 1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16) ## Hellion Chat 1.4.10 — Symbol-Picker and Tell-History Fix (2026-05-16)
Eleventh and final sub-patch of the v1.4.x Polish-Sweep series. Symbol picker for the chat input, a tell-history reload fix Eleventh and final sub-patch of the v1.4.x Polish-Sweep series. Symbol picker for the chat input, a tell-history reload fix
+30 -6
View File
@@ -10,13 +10,37 @@ the plugin's privacy-first scope during brainstorming.
--- ---
## Next Cycle (v1.5.0) ## Next Cycle (v1.5.1)
**DI-container adoption.** Microsoft.Extensions.Hosting plus `ILogger<T>` modelled on Lightless's `PluginHostFactory` **Honorific Full Gradient Port plus FontAtlas-Defer for a 10× HITCH cut.** v1.5.0 closed the DI-container cycle with
pattern. The v1.4.x Polish-Sweep series is closed; v1.5.0 starts the structural cycle that the smaller F12.x indirection no performance penalty against Chat 2 (77 ms vs 74 ms median first-frame HITCH), but the cross-plugin baseline against
shims (`IPluginLogProxy`, `IPlatformUtil`) were paving the way for. After that, the Wine/Linux scroll-rubber-band spike Lightless Sync and XIVInstantMessenger surfaced a clean optimisation: both plugins defer their font-atlas build until
deferred from v1.4.10 (Reserve-A cancelled — Windows users never saw it) plus the First-Run-Wizard rework that lets users after `Finished loading` and sit at 6-7 ms HITCH, an order of magnitude below the ~75 ms floor that Chat 2 and HellionChat
opt into the curated defaults instead of just picking a privacy profile. share. v1.5.1 ports that pattern. Plus the Honorific gradient render path — DTO is gradient-ready since v1.4.7, only the
Wave / Pulse animation port remains. After that, First-Run-Wizard rework with curated defaults beyond the three privacy
profiles, then FR localisation (Hezcal native-speaker review confirmed), then the Plugin Integrations Wave 2-6
(Context-Menu, NotificationMaster, Moodles, ExtraChat, XIVIM Quick-DM). Wine/Linux scroll-rubber-band spike sits as a
low-priority Linux-only investigation at the tail.
---
## v1.5.0 — DI Foundation and Service Refactor (released 2026-05-17)
Major architecture cycle. Plugin bootstrap moves to a generic-host DI container
(`Microsoft.Extensions.Hosting` + `IServiceCollection`) modelled on Lightless Sync's `PluginHostFactory`. Service
logging migrates from the static `Plugin.LogProxy` locator (the F12.2 shim from v1.4.7) to typed
`Microsoft.Extensions.Logging.ILogger<T>` via constructor injection, bridged over Dalamud's `IPluginLog` by a custom
`DalamudLogger` trio. 18 instance-class services move to ctor-injected loggers across four slices: data layer,
IPC/integrations, UI window layer, and root. `Plugin.LogProxy` stays for the eight buckets ctor injection cannot
reach — static helpers (`EmoteCache`, `AutoTranslate`, `MemoryUtil`, `WrapperUtil`), Dalamud-reflected types
(`Configuration`), the `Message` data class, and instance classes that only log from static methods (`FontManager`,
one `GameFunctions` site). Plugin.cs finishes at 1012 lines, virtually identical to the pre-cycle 1013 (-1 netto): the
new Phase-1 host build and `Plugin.X` bridge wiring trade out exactly the service and window allocations that previously
lived in `LoadAsync`. Cross-plugin baseline (10 reload-stress runs, 51 active plugins): HellionChat first-frame HITCH
77 ms median, Chat 2 v1.40.2 74 ms median — no DI penalty. The deferred-font-atlas pattern from Lightless and
XIVInstantMessenger is the v1.5.1 follow-up. User-visible: slash-command insert fix cherry-picked from ChatTwo upstream
`ee7768ac` — pasting a slash command into the chat input now replaces existing input instead of concatenating.
Migration v17 stays.
--- ---
+6 -6
View File
File diff suppressed because one or more lines are too long