Merge branch 'feature/v1.5.0'
This commit is contained in:
@@ -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.
|
||||
@@ -9,6 +9,7 @@ using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
@@ -19,6 +20,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
private readonly Plugin _plugin;
|
||||
private readonly MessageManager _messageManager;
|
||||
private readonly MessageStore _store;
|
||||
private readonly ILogger<AutoTellTabsService> _logger;
|
||||
private readonly object _tempTabsLock = new();
|
||||
|
||||
// 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;
|
||||
|
||||
internal AutoTellTabsService(Plugin plugin, MessageManager messageManager, MessageStore store)
|
||||
internal AutoTellTabsService(
|
||||
Plugin plugin,
|
||||
MessageManager messageManager,
|
||||
MessageStore store,
|
||||
ILogger<AutoTellTabsService> logger
|
||||
)
|
||||
{
|
||||
_plugin = plugin;
|
||||
_messageManager = messageManager;
|
||||
_store = store;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
// Derived from the tab list on read. Pin/Unpin/Promote/Logout simply
|
||||
@@ -67,7 +75,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
private void RehydratePinnedTabs()
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -76,7 +84,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
|
||||
if (tab.TellTarget is null || !tab.TellTarget.IsSet())
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
_logger.LogWarning(
|
||||
$"[Pin] Pinned tab '{tab.Name}' has no usable TellTarget "
|
||||
+ $"(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."
|
||||
@@ -93,7 +101,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
// sees the recent conversation, not a blank tab.
|
||||
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}"
|
||||
);
|
||||
}
|
||||
@@ -130,7 +138,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
if (partner == null)
|
||||
{
|
||||
// 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}, "
|
||||
+ $"senderChunks={message.Sender.Count}, contentChunks={message.Content.Count}, "
|
||||
+ $"senderSourcePayloads={message.SenderSource?.Payloads?.Count ?? 0}, "
|
||||
@@ -361,7 +369,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 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(
|
||||
MakeSystemMarker(HellionStrings.AutoTellTabs_HistoryLoadError),
|
||||
MessageManager.MessageDisplayLimit
|
||||
@@ -456,7 +464,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
{
|
||||
if (!tab.IsTempTab || tab.IsPinned)
|
||||
{
|
||||
Plugin.LogProxy.Debug(
|
||||
_logger.LogDebug(
|
||||
$"[Pin] TryPin skipped: IsTempTab={tab.IsTempTab} IsPinned={tab.IsPinned}"
|
||||
);
|
||||
return false;
|
||||
@@ -472,7 +480,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
|
||||
tab.IsPinned = true;
|
||||
Plugin.LogProxy.Debug(
|
||||
_logger.LogDebug(
|
||||
$"[Pin] Pinned tab '{tab.Name}' target={tab.TellTarget?.Name}@{tab.TellTarget?.World}"
|
||||
);
|
||||
_plugin.SaveConfig();
|
||||
@@ -495,7 +503,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
}
|
||||
|
||||
tab.IsPinned = false;
|
||||
Plugin.LogProxy.Debug("[Pin] Unpinned tab '{tab.Name}'");
|
||||
_logger.LogDebug("[Pin] Unpinned tab '{TabName}'", tab.Name);
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
|
||||
@@ -509,9 +517,7 @@ internal sealed class AutoTellTabsService : IDisposable
|
||||
tab.IsTempTab = false;
|
||||
tab.IsPinned = false;
|
||||
tab.TellTarget = TellTarget.Empty();
|
||||
Plugin.LogProxy.Debug(
|
||||
$"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)"
|
||||
);
|
||||
_logger.LogDebug($"[Pin] Promoted tab '{tab.Name}' to permanent (tell-binding dropped)");
|
||||
_plugin.SaveConfig();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
using Dalamud.Game.Command;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
internal sealed class Commands : IDisposable
|
||||
{
|
||||
private readonly Dictionary<string, CommandWrapper> Registered = [];
|
||||
private readonly ILogger<Commands> _logger;
|
||||
|
||||
public Commands(ILogger<Commands> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
@@ -52,7 +59,7 @@ internal sealed class Commands : IDisposable
|
||||
{
|
||||
if (!Registered.TryGetValue(command, out var wrapper))
|
||||
{
|
||||
Plugin.LogProxy.Warning($"Missing registration for command {command}");
|
||||
_logger.LogWarning($"Missing registration for command {command}");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +69,7 @@ internal sealed class Commands : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.LogProxy.Error(ex, $"Error while executing command {command}");
|
||||
_logger.LogError(ex, $"Error while executing command {command}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ using Dalamud.Interface.Utility;
|
||||
|
||||
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
|
||||
{
|
||||
internal IFontHandle Axis = null!;
|
||||
@@ -234,9 +237,9 @@ public class FontManager
|
||||
or ArgumentException
|
||||
)
|
||||
{
|
||||
// Atlas-toolkit throws span IO and validation failures; routing the
|
||||
// wider set through the fallback keeps a corrupt font config from
|
||||
// taking down the whole atlas build.
|
||||
// Atlas-toolkit throws span IO and validation failures; routing
|
||||
// the wider set through the fallback keeps a corrupt font config
|
||||
// from taking down the whole atlas build.
|
||||
Plugin.LogProxy.Warning(
|
||||
e,
|
||||
$"Configured {slot} font failed to load ({e.GetType().Name}), "
|
||||
|
||||
@@ -19,6 +19,7 @@ using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using InteropGenerator.Runtime;
|
||||
using Lumina.Text.ReadOnly;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
||||
|
||||
namespace HellionChat.GameFunctions;
|
||||
@@ -98,9 +99,12 @@ internal sealed unsafe class Chat : IDisposable
|
||||
private long LastPlayerNameDisplayTypeRefresh;
|
||||
private PlayerNameDisplayType CurrentPlayerNameDisplayType = PlayerNameDisplayType.FullName;
|
||||
|
||||
public Chat(Plugin plugin)
|
||||
private readonly ILogger<Chat> _logger;
|
||||
|
||||
public Chat(Plugin plugin, ILogger<Chat> logger)
|
||||
{
|
||||
Plugin = plugin;
|
||||
_logger = logger;
|
||||
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
||||
|
||||
ChatLogRefreshHook?.Enable();
|
||||
@@ -236,7 +240,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
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)
|
||||
{
|
||||
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
|
||||
@@ -299,7 +303,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
{
|
||||
playerName = SeString.Parse(agent->TellPlayerName).TextValue;
|
||||
worldId = agent->TellWorldId;
|
||||
Plugin.LogProxy.Debug($"Detected tell target '[redacted]'@{worldId}");
|
||||
_logger.LogDebug($"Detected tell target '[redacted]'@{worldId}");
|
||||
}
|
||||
|
||||
Plugin.CurrentTab.CurrentChannel = new UsedChannel
|
||||
@@ -358,7 +362,7 @@ internal sealed unsafe class Chat : IDisposable
|
||||
}
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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."
|
||||
);
|
||||
return;
|
||||
|
||||
@@ -14,6 +14,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Info;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.AtkValueType;
|
||||
|
||||
namespace HellionChat.GameFunctions;
|
||||
@@ -37,14 +38,20 @@ internal unsafe class GameFunctions : IDisposable
|
||||
#endregion
|
||||
|
||||
private Plugin Plugin { get; }
|
||||
private readonly ILogger<GameFunctions> _logger;
|
||||
internal KeybindManager KeybindManager { get; }
|
||||
internal Chat Chat { get; }
|
||||
|
||||
internal GameFunctions(Plugin plugin)
|
||||
internal GameFunctions(
|
||||
Plugin plugin,
|
||||
ILogger<GameFunctions> logger,
|
||||
ILoggerFactory loggerFactory
|
||||
)
|
||||
{
|
||||
Plugin = plugin;
|
||||
KeybindManager = new KeybindManager(plugin);
|
||||
Chat = new Chat(Plugin);
|
||||
_logger = logger;
|
||||
KeybindManager = new KeybindManager(plugin, loggerFactory.CreateLogger<KeybindManager>());
|
||||
Chat = new Chat(Plugin, loggerFactory.CreateLogger<Chat>());
|
||||
|
||||
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
||||
ResolveTextCommandPlaceholderHook?.Enable();
|
||||
@@ -215,6 +222,7 @@ internal unsafe class GameFunctions : IDisposable
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Static method, no instance _logger reachable here.
|
||||
Plugin.LogProxy.Warning(e, "Unable to open adventurer plate");
|
||||
return false;
|
||||
}
|
||||
@@ -255,7 +263,7 @@ internal unsafe class GameFunctions : IDisposable
|
||||
var byteCount = System.Text.Encoding.UTF8.GetByteCount(ReplacementName);
|
||||
if (byteCount >= PlaceholderBufferSize)
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
_logger.LogWarning(
|
||||
$"Replacement name too long for placeholder buffer ({byteCount} bytes >= {PlaceholderBufferSize}); falling back to original."
|
||||
);
|
||||
ReplacementName = null;
|
||||
|
||||
@@ -8,6 +8,7 @@ using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using HellionChat.Code;
|
||||
using HellionChat.GameFunctions.Types;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ModifierFlag = HellionChat.GameFunctions.Types.ModifierFlag;
|
||||
|
||||
namespace HellionChat.GameFunctions;
|
||||
@@ -306,9 +307,12 @@ internal unsafe class KeybindManager : IDisposable
|
||||
// VirtualKey.OEM_CLEAR,
|
||||
};
|
||||
|
||||
internal KeybindManager(Plugin plugin)
|
||||
private readonly ILogger<KeybindManager> _logger;
|
||||
|
||||
internal KeybindManager(Plugin plugin, ILogger<KeybindManager> logger)
|
||||
{
|
||||
Plugin = plugin;
|
||||
_logger = logger;
|
||||
Plugin.GameInteropProvider.InitializeFromAttributes(this);
|
||||
|
||||
// Handle keybinds from the game on every tick.
|
||||
@@ -507,7 +511,7 @@ internal unsafe class KeybindManager : IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.LogProxy.Error(ex, "Error in chat Activated event");
|
||||
_logger.LogError(ex, "Error in chat Activated event");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Dalamud.NET.Sdk/15.0.0">
|
||||
<PropertyGroup>
|
||||
<!-- Independent versioning; see yaml changelog for upstream Chat 2 base -->
|
||||
<Version>1.4.10</Version>
|
||||
<Version>1.5.0</Version>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<!-- Use lock file to pin exact versions -->
|
||||
@@ -15,6 +15,14 @@
|
||||
<!-- Closed ranges prevent surprise major bumps during lock file regeneration -->
|
||||
<PackageReference Include="MessagePack" Version="[3.1.4, 4.0.0)" />
|
||||
<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) -->
|
||||
<PackageReference Include="SQLitePCLRaw.lib.e_sqlite3" Version="3.50.3" />
|
||||
<PackageReference Include="morelinq" Version="4.4.0" />
|
||||
|
||||
@@ -35,6 +35,54 @@ tags:
|
||||
- Replacement
|
||||
- Privacy
|
||||
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)**
|
||||
|
||||
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
|
||||
44–160 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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace HellionChat.Integrations;
|
||||
@@ -23,7 +24,7 @@ internal sealed class HonorificService : IDisposable
|
||||
private readonly ICallGateSubscriber<object> _ready;
|
||||
private readonly ICallGateSubscriber<object> _disposing;
|
||||
|
||||
private readonly IPluginLog _log;
|
||||
private readonly ILogger<HonorificService> _logger;
|
||||
private readonly IFramework _framework;
|
||||
private bool _versionWarningLogged;
|
||||
|
||||
@@ -34,12 +35,12 @@ internal sealed class HonorificService : IDisposable
|
||||
|
||||
public HonorificService(
|
||||
IDalamudPluginInterface pluginInterface,
|
||||
IPluginLog log,
|
||||
ILogger<HonorificService> logger,
|
||||
IFramework framework
|
||||
)
|
||||
{
|
||||
_framework = framework;
|
||||
_log = log;
|
||||
_logger = logger;
|
||||
|
||||
// Gate objects are cached per-name by Dalamud and safe to register
|
||||
// before Honorific loads — they just won't fire until it does.
|
||||
@@ -84,7 +85,7 @@ internal sealed class HonorificService : IDisposable
|
||||
{
|
||||
if (!_versionWarningLogged)
|
||||
{
|
||||
_log.Warning(
|
||||
_logger.LogWarning(
|
||||
"Honorific API version mismatch — expected major 3, "
|
||||
+ "found {Major}.{Minor}. Disabling Honorific integration.",
|
||||
version.Item1,
|
||||
@@ -104,7 +105,7 @@ internal sealed class HonorificService : IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 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;
|
||||
CurrentTitle = null;
|
||||
}
|
||||
@@ -149,7 +150,7 @@ internal sealed class HonorificService : IDisposable
|
||||
{
|
||||
// Warning not Debug — a silent unsubscribe failure leaks a live
|
||||
// subscription across plugin reloads.
|
||||
_log.Warning(
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Honorific unsubscribe failed (likely API break or gate already gone)."
|
||||
);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Ipc;
|
||||
|
||||
public sealed class ExtraChat : IDisposable
|
||||
{
|
||||
private readonly ILogger<ExtraChat> _logger;
|
||||
|
||||
#pragma warning disable CS0649 // Assigned through IPC
|
||||
[Serializable]
|
||||
private struct OverrideInfo
|
||||
@@ -36,8 +39,9 @@ public sealed class ExtraChat : IDisposable
|
||||
private volatile Dictionary<Guid, string> ChannelNamesInternal = new();
|
||||
internal IReadOnlyDictionary<Guid, string> ChannelNames => ChannelNamesInternal;
|
||||
|
||||
internal ExtraChat()
|
||||
internal ExtraChat(ILogger<ExtraChat> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
OverrideChannelGate = Plugin.Interface.GetIpcSubscriber<OverrideInfo, object>(
|
||||
"ExtraChat.OverrideChannelColour"
|
||||
);
|
||||
@@ -62,10 +66,7 @@ public sealed class ExtraChat : IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ExtraChat is optional; IPC failure is normal when the plugin isn't loaded.
|
||||
Plugin.LogProxy.Verbose(
|
||||
ex,
|
||||
"ExtraChat IPC initial state query failed (peer not loaded?)"
|
||||
);
|
||||
_logger.LogTrace(ex, "ExtraChat IPC initial state query failed (peer not loaded?)");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using HellionChat.Code;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Ipc;
|
||||
|
||||
@@ -33,9 +34,12 @@ internal sealed class TypingIpc : IDisposable
|
||||
private ChatInputState LastState;
|
||||
private bool HasState;
|
||||
|
||||
internal TypingIpc(Plugin plugin)
|
||||
private readonly ILogger<TypingIpc> _logger;
|
||||
|
||||
internal TypingIpc(Plugin plugin, ILogger<TypingIpc> logger)
|
||||
{
|
||||
Plugin = plugin;
|
||||
_logger = logger;
|
||||
|
||||
StateQueryGate = Plugin.Interface.GetIpcProvider<ChatInputState>(
|
||||
"HellionChat.GetChatInputState"
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
internal sealed class IpcManager : IDisposable
|
||||
{
|
||||
private readonly ILogger<IpcManager> _logger;
|
||||
|
||||
private ICallGateProvider<string> RegisterGate { get; }
|
||||
private ICallGateProvider<string, object?> UnregisterGate { get; }
|
||||
private ICallGateProvider<object?> AvailableGate { get; }
|
||||
@@ -41,8 +44,9 @@ internal sealed class IpcManager : IDisposable
|
||||
|
||||
internal List<string> Registered { get; } = [];
|
||||
|
||||
public IpcManager()
|
||||
public IpcManager(ILogger<IpcManager> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
RegisterGate = Plugin.Interface.GetIpcProvider<string>("HellionChat.Register");
|
||||
RegisterGate.RegisterFunc(Register);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ using HellionChat.Util;
|
||||
using Lumina.Text.Expressions;
|
||||
using Lumina.Text.Payloads;
|
||||
using Lumina.Text.ReadOnly;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
@@ -22,6 +23,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
internal const int MessageDisplayLimit = 10_000;
|
||||
|
||||
private Plugin Plugin { get; }
|
||||
private readonly ILogger<MessageManager> _logger;
|
||||
internal MessageStore Store { get; }
|
||||
|
||||
private Dictionary<ChatType, NameFormatting> Formats { get; } = [];
|
||||
@@ -48,11 +50,21 @@ internal class MessageManager : IAsyncDisposable
|
||||
// AutoTellTabsService to spawn or refresh temp tabs without coupling.
|
||||
public event Action<Message>? MessageProcessed;
|
||||
|
||||
internal unsafe MessageManager(Plugin plugin)
|
||||
internal unsafe MessageManager(
|
||||
Plugin plugin,
|
||||
ILogger<MessageManager> logger,
|
||||
ILoggerFactory loggerFactory
|
||||
)
|
||||
{
|
||||
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(() =>
|
||||
ProcessPendingMessages(PendingThreadCancellationToken.Token)
|
||||
@@ -91,7 +103,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
await Task.Delay(100);
|
||||
|
||||
if (PendingMessageThread.IsAlive)
|
||||
Plugin.LogProxy.Warning(
|
||||
_logger.LogWarning(
|
||||
"PendingMessageThread did not observe cancellation within 10s. "
|
||||
+ "Worker remains on background thread; next plugin reload releases it."
|
||||
);
|
||||
@@ -137,7 +149,7 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.LogProxy.Error(ex, "Error processing pending message");
|
||||
_logger.LogError(ex, "Error processing pending message");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -182,12 +194,12 @@ internal class MessageManager : IAsyncDisposable
|
||||
|
||||
// Mark failed messages as deleted to prevent retry attempts
|
||||
var failedIds = messages.FailedMessageIds();
|
||||
Plugin.LogProxy.Info(
|
||||
_logger.LogInformation(
|
||||
$"Marking {failedIds.Count} messages as deleted due to parse failures"
|
||||
);
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -203,13 +215,13 @@ internal class MessageManager : IAsyncDisposable
|
||||
}
|
||||
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
|
||||
// without a Debug filter. Belt-and-suspenders for future plugin-load
|
||||
// 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)
|
||||
{
|
||||
Plugin.LogProxy.Error(ex, "Error in ContentIdResolver");
|
||||
_logger.LogError(ex, "Error in ContentIdResolver");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+46
-21
@@ -9,6 +9,7 @@ using MessagePack;
|
||||
using MessagePack.Formatters;
|
||||
using MessagePack.Resolvers;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Encoding = System.Text.Encoding;
|
||||
|
||||
namespace HellionChat;
|
||||
@@ -179,7 +180,8 @@ internal class MessageStore : IDisposable
|
||||
}
|
||||
|
||||
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
|
||||
// per-frame IsFtsIndexBuilt read sees the flip the moment the bulk-insert
|
||||
@@ -197,11 +199,17 @@ internal class MessageStore : IDisposable
|
||||
// own SqliteConnection via OpenSecondaryConnection.
|
||||
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;
|
||||
_platformUtil = platformUtil;
|
||||
_logger = logger;
|
||||
_loggerFactory = loggerFactory;
|
||||
Connection = Connect();
|
||||
Migrate();
|
||||
InitFtsReadyCache();
|
||||
@@ -246,7 +254,7 @@ internal class MessageStore : IDisposable
|
||||
conn.Open();
|
||||
ApplyPragmas(conn);
|
||||
connectSw.Stop();
|
||||
_logger.Information($"MessageStore.Connect took {connectSw.ElapsedMilliseconds}ms");
|
||||
_logger.LogInformation($"MessageStore.Connect took {connectSw.ElapsedMilliseconds}ms");
|
||||
return conn;
|
||||
}
|
||||
|
||||
@@ -290,12 +298,12 @@ internal class MessageStore : IDisposable
|
||||
migration();
|
||||
|
||||
migrateSw.Stop();
|
||||
_logger.Information($"MessageStore.Migrate took {migrateSw.ElapsedMilliseconds}ms");
|
||||
_logger.LogInformation($"MessageStore.Migrate took {migrateSw.ElapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
private void Migrate0()
|
||||
{
|
||||
_logger.Information("Running migration 0: Creating tables");
|
||||
_logger.LogInformation("Running migration 0: Creating tables");
|
||||
Connection.Execute(
|
||||
@"
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
@@ -322,7 +330,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate1()
|
||||
{
|
||||
_logger.Information("Running migration 1: Adding Deleted column");
|
||||
_logger.LogInformation("Running migration 1: Adding Deleted column");
|
||||
Connection.Execute(
|
||||
@"
|
||||
ALTER TABLE messages ADD COLUMN Deleted BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -334,7 +342,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
private void Migrate2()
|
||||
{
|
||||
_logger.Information("Running migration 2: Adding Channel generated column");
|
||||
_logger.LogInformation("Running migration 2: Adding Channel generated column");
|
||||
Connection.Execute(
|
||||
@"
|
||||
ALTER TABLE messages ADD COLUMN Channel INTEGER GENERATED ALWAYS AS (Code & 0x7f) VIRTUAL;
|
||||
@@ -362,13 +370,15 @@ internal class MessageStore : IDisposable
|
||||
|
||||
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
|
||||
// shape but user_version was never bumped -- just record and exit.
|
||||
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);
|
||||
return;
|
||||
}
|
||||
@@ -398,7 +408,7 @@ internal class MessageStore : IDisposable
|
||||
|
||||
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).
|
||||
// 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)
|
||||
{
|
||||
_logger.Information($"Setting version {version}");
|
||||
_logger.LogInformation($"Setting version {version}");
|
||||
using var cmd = Connection.CreateCommand();
|
||||
// PRAGMA does not accept SQLite parameter bindings; version is a
|
||||
// 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.
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -941,7 +951,10 @@ internal class MessageStore : IDisposable
|
||||
if (to is not null)
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
{
|
||||
if (!ChunkUtil.MatchesSender(message, senderName, senderWorld))
|
||||
@@ -1145,7 +1164,10 @@ internal class MessageStore : IDisposable
|
||||
((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("$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>,
|
||||
IDisposable,
|
||||
IAsyncDisposable
|
||||
{
|
||||
private const int MaxErrorLogs = 10;
|
||||
|
||||
private readonly IPluginLogProxy _logger = logger;
|
||||
private readonly ILogger<MessageEnumerator> _logger = logger;
|
||||
private readonly List<Guid> FailedIds = [];
|
||||
private int FailedCount;
|
||||
public bool DidError => FailedCount > 0;
|
||||
@@ -1247,10 +1272,10 @@ internal class MessageEnumerator(DbDataReader reader, IPluginLogProxy logger)
|
||||
catch (Exception e)
|
||||
{
|
||||
if (FailedCount < MaxErrorLogs)
|
||||
_logger.Error($"Exception while reading message '{id}' from database: {e}");
|
||||
_logger.LogError($"Exception while reading message '{id}' from database: {e}");
|
||||
FailedCount++;
|
||||
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)
|
||||
FailedIds.Add(id);
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ using HellionChat.Resources;
|
||||
using HellionChat.Ui;
|
||||
using HellionChat.Util;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Action = System.Action;
|
||||
using ChatTwoPartyFinderPayload = HellionChat.Util.PartyFinderPayload;
|
||||
using DalamudPartyFinderPayload = Dalamud.Game.Text.SeStringHandling.Payloads.PartyFinderPayload;
|
||||
@@ -40,9 +41,12 @@ public sealed class PayloadHandler
|
||||
|
||||
private const uint PopupSfx = 1;
|
||||
|
||||
internal PayloadHandler(ChatLogWindow logWindow)
|
||||
private readonly ILogger<PayloadHandler> _logger;
|
||||
|
||||
internal PayloadHandler(ChatLogWindow logWindow, ILogger<PayloadHandler> logger)
|
||||
{
|
||||
LogWindow = logWindow;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
internal void Draw()
|
||||
@@ -131,7 +135,7 @@ public sealed class PayloadHandler
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -546,7 +550,7 @@ public sealed class PayloadHandler
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.LogProxy.Error(ex, "Error executing DalamudLinkPayload handler");
|
||||
_logger.LogError(ex, "Error executing DalamudLinkPayload handler");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+99
-100
@@ -15,6 +15,8 @@ using HellionChat.Resources;
|
||||
using HellionChat.Ui;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace HellionChat;
|
||||
|
||||
@@ -123,6 +125,11 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
// isolation. Wired immediately after Dalamud injects Log.
|
||||
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
|
||||
// re-registering with identical args (v1.4.9 ISSUE-1 cleanup).
|
||||
private CommandWrapper? _hellionSettingsCmd;
|
||||
@@ -185,11 +192,11 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
Config = Interface.GetPluginConfig() as Configuration ?? new Configuration();
|
||||
|
||||
// Wire platform indirection before LoadAsync allocates anything that
|
||||
// needs Util.* — services then read Plugin.PlatformUtil instead of
|
||||
// hitting the Dalamud static surface directly.
|
||||
PlatformUtil = new DalamudPlatformUtil();
|
||||
LogProxy = new DalamudPluginLogProxy(Log);
|
||||
// PlatformUtil and LogProxy are filled from the DI container in
|
||||
// Phase-1 below (`_host.Services.GetRequiredService<IPlatformUtil>()`
|
||||
// and the LogProxy equivalent). Phase-0 helpers that run before that
|
||||
// point (MigrateFromChatTwoLayout, LanguageChanged, ImGuiUtil.Initialize)
|
||||
// do not touch either static, so the brief null-window is safe.
|
||||
|
||||
// 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
|
||||
@@ -212,6 +219,72 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
ImGuiUtil.Initialize(this);
|
||||
|
||||
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)
|
||||
@@ -233,66 +306,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// BuildFonts registers handles with Dalamud's FontAtlas; the atlas
|
||||
// rebuilds async a few frames later (visible "font-pop" on first load).
|
||||
FontManager = new FontManager();
|
||||
FontManager.BuildFonts();
|
||||
|
||||
// ThemeRegistry must be wired before the first Draw tick.
|
||||
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||
Directory.CreateDirectory(customThemesDir);
|
||||
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();
|
||||
// Container drives service init now: Host.StartAsync triggers the
|
||||
// IHostedService adapters (FontManager.BuildFonts, ThemeRegistry
|
||||
// cache warmup + Switch, IPC eager-resolve, MessageManager
|
||||
// FilterAllTabsAsync, AutoTellTabsService.Initialize). Window
|
||||
// registration with WindowSystem runs on the framework thread
|
||||
// inside PluginLifecycle.LoadAsync after StartAsync returns.
|
||||
if (_lifecycle is not null)
|
||||
await _lifecycle.LoadAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
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)
|
||||
FirstRunWizard.IsOpen = true;
|
||||
|
||||
@@ -313,8 +337,8 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
if (Config.ShowEmotes)
|
||||
_ = EmoteCache.LoadData();
|
||||
|
||||
if (Interface.Reason is not PluginLoadReason.Boot)
|
||||
MessageManager.FilterAllTabsAsync();
|
||||
// FilterAllTabsAsync now runs from MessageManagerInitHostedService
|
||||
// during Host.StartAsync (same Reason-not-Boot guard there).
|
||||
|
||||
// Kick the FTS5 rebuild worker if Migrate4 just added the schema or
|
||||
// a previous run was cut short (InitFtsReadyCache leaves _ftsReady
|
||||
@@ -499,49 +523,18 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
}
|
||||
);
|
||||
|
||||
// Unsubscribe AutoTellTabs before MessageManager goes away.
|
||||
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.
|
||||
// Framework-thread cleanup the container does not reach.
|
||||
try
|
||||
{
|
||||
await Framework
|
||||
.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,
|
||||
() => 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, () => 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);
|
||||
}
|
||||
@@ -550,11 +543,17 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
||||
failure ??= ex;
|
||||
}
|
||||
|
||||
// Pure-memory cleanups — no Framework / UI / IPC touch.
|
||||
failure = CaptureFailure(failure, () => Functions?.Dispose());
|
||||
failure = CaptureFailure(failure, () => Commands?.Dispose());
|
||||
// Container disposes services + windows on the framework thread.
|
||||
// MessageManager.DisposeAsync is not idempotent, so we let the
|
||||
// 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());
|
||||
// Static input history would otherwise survive the plugin reload.
|
||||
failure = CaptureFailure(failure, InputHistoryService.Reset);
|
||||
|
||||
if (failure is not null)
|
||||
|
||||
@@ -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
|
||||
);
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
using HellionChat.Themes.Builtin;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Themes;
|
||||
|
||||
public sealed class ThemeRegistry
|
||||
{
|
||||
private readonly ILogger<ThemeRegistry>? _logger;
|
||||
|
||||
public const string DefaultSlug = HellionArctic.Slug;
|
||||
|
||||
// 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 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).
|
||||
// Row 1: blue family. Row 2: purple to magenta family.
|
||||
// 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))
|
||||
{
|
||||
// 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"
|
||||
);
|
||||
if (cached.Theme is not null)
|
||||
|
||||
@@ -22,6 +22,7 @@ using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Lumina.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
@@ -98,10 +99,19 @@ public sealed class ChatLogWindow : Window
|
||||
private long FrameTime; // set every frame
|
||||
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")
|
||||
{
|
||||
Plugin = plugin;
|
||||
_logger = logger;
|
||||
_loggerFactory = loggerFactory;
|
||||
Salt = new Random().Next().ToString();
|
||||
|
||||
Size = new Vector2(500, 250);
|
||||
@@ -114,8 +124,10 @@ public sealed class ChatLogWindow : Window
|
||||
DisableWindowSounds = true;
|
||||
// AllowBackgroundBlur is set centrally in Plugin.Setup after AddWindow.
|
||||
|
||||
PayloadHandler = new PayloadHandler(this);
|
||||
HandlerLender = new Lender<PayloadHandler>(() => new PayloadHandler(this));
|
||||
PayloadHandler = new PayloadHandler(this, _loggerFactory.CreateLogger<PayloadHandler>());
|
||||
HandlerLender = new Lender<PayloadHandler>(() =>
|
||||
new PayloadHandler(this, _loggerFactory.CreateLogger<PayloadHandler>())
|
||||
);
|
||||
|
||||
SetUpTextCommandChannels();
|
||||
SetUpAllCommands();
|
||||
@@ -192,11 +204,28 @@ public sealed class ChatLogWindow : Window
|
||||
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))
|
||||
Chat += args.AddIfNotPresent;
|
||||
{
|
||||
if (args.AddIfNotPresent.StartsWith('/'))
|
||||
Chat = args.AddIfNotPresent;
|
||||
else
|
||||
Chat += args.AddIfNotPresent;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -280,7 +309,7 @@ public sealed class ChatLogWindow : Window
|
||||
|| !GameFunctions.Chat.IsChannelOrExistingLinkshell(targetChannel.Value)
|
||||
)
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
_logger.LogWarning(
|
||||
$"Channel was set to an invalid value '{targetChannel}', ignoring"
|
||||
);
|
||||
return;
|
||||
@@ -334,11 +363,11 @@ public sealed class ChatLogWindow : Window
|
||||
{
|
||||
case "hide":
|
||||
CurrentHideState = HideState.User;
|
||||
Plugin.LogProxy.Verbose("HideState: → User (chat hide command)");
|
||||
_logger.LogTrace("HideState: → User (chat hide command)");
|
||||
break;
|
||||
case "show":
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.LogProxy.Verbose("HideState: → None (chat show command)");
|
||||
_logger.LogTrace("HideState: → None (chat show command)");
|
||||
break;
|
||||
case "toggle":
|
||||
CurrentHideState = CurrentHideState switch
|
||||
@@ -348,7 +377,7 @@ public sealed class ChatLogWindow : Window
|
||||
HideState.None => HideState.User,
|
||||
_ => CurrentHideState,
|
||||
};
|
||||
Plugin.LogProxy.Verbose($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||
_logger.LogTrace($"HideState: → {CurrentHideState} (chat toggle command)");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -458,7 +487,7 @@ public sealed class ChatLogWindow : Window
|
||||
else if (newTab.CurrentChannel.Channel is InputChannel.Invalid)
|
||||
{
|
||||
newTab.CurrentChannel = previousTab.CurrentChannel.Clone();
|
||||
Plugin.LogProxy.Debug(
|
||||
_logger.LogDebug(
|
||||
$"[Tab] '{newTab.Name}' seeded channel from '{previousTab.Name}' "
|
||||
+ $"(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)
|
||||
{
|
||||
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 (CurrentHideState is HideState.Battle && !Plugin.InBattle)
|
||||
{
|
||||
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
|
||||
@@ -506,7 +535,7 @@ public sealed class ChatLogWindow : Window
|
||||
if (Plugin.Functions.Chat.CheckHideFlags())
|
||||
{
|
||||
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.LogProxy.Verbose($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||
_logger.LogTrace($"HideState: {CurrentHideState} → None (cutscene/gpose ended)");
|
||||
CurrentHideState = HideState.None;
|
||||
}
|
||||
|
||||
@@ -525,14 +554,14 @@ public sealed class ChatLogWindow : Window
|
||||
if (CurrentHideState == HideState.Cutscene && Activate)
|
||||
{
|
||||
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 (CurrentHideState == HideState.User && Activate)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.LogProxy.Verbose("HideState: User → None (activate)");
|
||||
_logger.LogTrace("HideState: User → None (activate)");
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -663,7 +692,7 @@ public sealed class ChatLogWindow : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.LogProxy.Error(ex, "Error drawing Chat Log window");
|
||||
_logger.LogError(ex, "Error drawing Chat Log window");
|
||||
if (!NotifiedDrawFailure)
|
||||
{
|
||||
Plugin.Notification.AddNotification(
|
||||
@@ -1705,7 +1734,7 @@ public sealed class ChatLogWindow : Window
|
||||
}
|
||||
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.SaveConfig();
|
||||
Plugin.LogProxy.Debug("v0.6.1 pop-out header hint dismissed");
|
||||
_logger.LogDebug("v0.6.1 pop-out header hint dismissed");
|
||||
if (openSettings)
|
||||
Plugin.SettingsWindow.Toggle();
|
||||
}
|
||||
@@ -2391,7 +2420,7 @@ public sealed class ChatLogWindow : Window
|
||||
if (PopOutWindows.Contains(tab.Identifier))
|
||||
continue;
|
||||
|
||||
var window = new Popout(this, tab, i);
|
||||
var window = new Popout(this, tab, i, _loggerFactory.CreateLogger<Popout>());
|
||||
|
||||
Plugin.WindowSystem.AddWindow(window);
|
||||
PopOutWindows.Add(tab.Identifier);
|
||||
@@ -2908,7 +2937,7 @@ public sealed class ChatLogWindow : Window
|
||||
var viewport = ImGui.GetMainViewport();
|
||||
var safePos = viewport.WorkPos + SafeDefaultOffset;
|
||||
Position = safePos;
|
||||
Plugin.LogProxy.Info(
|
||||
_logger.LogInformation(
|
||||
$"[Window-Recovery] {source}: snapping main window from {LastWindowPos} (size {LastWindowSize}) to {safePos}."
|
||||
);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Lumina.Data.Files;
|
||||
using Lumina.Text.ReadOnly;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MoreLinq;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
@@ -67,10 +68,13 @@ public class DbViewer : Window
|
||||
|
||||
private bool NeedsScrollReset;
|
||||
|
||||
public DbViewer(Plugin plugin)
|
||||
private readonly ILogger<DbViewer> _logger;
|
||||
|
||||
public DbViewer(Plugin plugin, ILogger<DbViewer> logger)
|
||||
: base("DBViewer###chat2-dbviewer")
|
||||
{
|
||||
Plugin = plugin;
|
||||
_logger = logger;
|
||||
SelectedChannels = TabsUtil.MostlyPlayer;
|
||||
|
||||
DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
||||
@@ -320,7 +324,7 @@ public class DbViewer : Window
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Plugin.LogProxy.Error(ex, "Failed reading messages from database");
|
||||
_logger.LogError(ex, "Failed reading messages from database");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -483,7 +487,7 @@ public class DbViewer : Window
|
||||
}
|
||||
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)
|
||||
{
|
||||
Plugin.LogProxy.Error(ex, "Failed creating txt backup");
|
||||
_logger.LogError(ex, "Failed creating txt backup");
|
||||
|
||||
Notification.Content = "Error ...";
|
||||
Notification.Type = NotificationType.Error;
|
||||
|
||||
@@ -3,6 +3,7 @@ using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Style;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
@@ -11,6 +12,7 @@ internal class Popout : Window
|
||||
private readonly ChatLogWindow ChatLogWindow;
|
||||
private readonly Tab Tab;
|
||||
private readonly int Idx;
|
||||
private readonly ILogger<Popout> _logger;
|
||||
|
||||
private long FrameTime;
|
||||
private long LastActivityTime = Environment.TickCount64;
|
||||
@@ -23,12 +25,13 @@ internal class Popout : Window
|
||||
// Exposed so AutoTellTabsService can locate this window during LRU eviction.
|
||||
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")
|
||||
{
|
||||
ChatLogWindow = chatLogWindow;
|
||||
Tab = tab;
|
||||
Idx = idx;
|
||||
_logger = logger;
|
||||
|
||||
Size = new Vector2(350, 350);
|
||||
SizeCondition = ImGuiCond.FirstUseEver;
|
||||
@@ -175,7 +178,7 @@ internal class Popout : Window
|
||||
{
|
||||
Plugin.Config.SeenPopOutInputHint = true;
|
||||
ChatLogWindow.Plugin.SaveConfig();
|
||||
Plugin.LogProxy.Debug("Pop-Out input hint dismissed");
|
||||
_logger.LogDebug("Pop-Out input hint dismissed");
|
||||
if (openSettings)
|
||||
ChatLogWindow.Plugin.SettingsWindow.Toggle();
|
||||
}
|
||||
@@ -214,13 +217,13 @@ internal class Popout : Window
|
||||
if (Tab.HideInBattle && CurrentHideState == HideState.None && Plugin.InBattle)
|
||||
{
|
||||
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)
|
||||
{
|
||||
CurrentHideState = HideState.None;
|
||||
Plugin.LogProxy.Verbose($"Popout HideState [{Tab.Name}]: Battle -> None");
|
||||
_logger.LogTrace($"Popout HideState [{Tab.Name}]: Battle -> None");
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -232,7 +235,7 @@ internal class Popout : Window
|
||||
if (ChatLogWindow.Plugin.Functions.Chat.CheckHideFlags())
|
||||
{
|
||||
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.LogProxy.Verbose(
|
||||
_logger.LogTrace(
|
||||
$"Popout HideState [{Tab.Name}]: {CurrentHideState} -> None (cutscene/gpose ended)"
|
||||
);
|
||||
CurrentHideState = HideState.None;
|
||||
@@ -251,7 +254,7 @@ internal class Popout : Window
|
||||
if (CurrentHideState == HideState.Cutscene && ChatLogWindow.Activate)
|
||||
{
|
||||
CurrentHideState = HideState.CutsceneOverride;
|
||||
Plugin.LogProxy.Verbose(
|
||||
_logger.LogTrace(
|
||||
$"Popout HideState [{Tab.Name}]: Cutscene -> CutsceneOverride (user activate)"
|
||||
);
|
||||
}
|
||||
@@ -259,7 +262,7 @@ internal class Popout : Window
|
||||
if (CurrentHideState == HideState.User && ChatLogWindow.Activate)
|
||||
{
|
||||
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
|
||||
|
||||
@@ -6,6 +6,7 @@ using Dalamud.Utility;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Ui.SettingsTabs;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Ui;
|
||||
|
||||
@@ -25,7 +26,7 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
private SettingsView View = SettingsView.Overview;
|
||||
private readonly SettingsOverview Overview;
|
||||
|
||||
internal SettingsWindow(Plugin plugin)
|
||||
internal SettingsWindow(Plugin plugin, ILoggerFactory loggerFactory)
|
||||
: base($"{Language.Settings_Title.Format(Plugin.PluginName)}###chat2-settings")
|
||||
{
|
||||
Flags = ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse;
|
||||
@@ -45,13 +46,13 @@ public sealed class SettingsWindow : Dalamud.Interface.Windowing.Window
|
||||
Tabs =
|
||||
[
|
||||
new General(Plugin, Mutable),
|
||||
new ThemeAndLayout(Plugin, Mutable),
|
||||
new FontsAndColours(Plugin, Mutable),
|
||||
new ThemeAndLayout(Plugin, Mutable, loggerFactory.CreateLogger<ThemeAndLayout>()),
|
||||
new FontsAndColours(Plugin, Mutable, loggerFactory.CreateLogger<FontsAndColours>()),
|
||||
new SettingsTabs.Window(Plugin, Mutable),
|
||||
new Chat(Plugin, Mutable),
|
||||
new SettingsTabs.Tabs(Plugin, Mutable),
|
||||
new SettingsTabs.Privacy(Plugin, Mutable),
|
||||
new DataManagement(Plugin, Mutable),
|
||||
new DataManagement(Plugin, Mutable, loggerFactory.CreateLogger<DataManagement>()),
|
||||
new SettingsTabs.Integrations(Plugin, Mutable),
|
||||
new Information(Mutable),
|
||||
];
|
||||
|
||||
@@ -11,6 +11,7 @@ using HellionChat.Export;
|
||||
using HellionChat.Privacy;
|
||||
using HellionChat.Resources;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Ui.SettingsTabs;
|
||||
|
||||
@@ -18,6 +19,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
{
|
||||
private Plugin Plugin { get; }
|
||||
private Configuration Mutable { get; }
|
||||
private readonly ILogger<DataManagement> _logger;
|
||||
|
||||
public string Name =>
|
||||
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;
|
||||
Mutable = mutable;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
@@ -229,7 +232,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.LogProxy.Error(e, "Unable to delete old database");
|
||||
_logger.LogError(e, "Unable to delete old database");
|
||||
WrapperUtil.AddNotification(
|
||||
Language.Options_Database_Old_Delete_Error,
|
||||
NotificationType.Error
|
||||
@@ -391,9 +394,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
Plugin.Config.RetentionLastRunAt = DateTimeOffset.UtcNow;
|
||||
Plugin.SaveConfig();
|
||||
|
||||
Plugin.LogProxy.Information(
|
||||
$"Manual retention run deleted {deleted} expired messages."
|
||||
);
|
||||
_logger.LogInformation($"Manual retention run deleted {deleted} expired messages.");
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
@@ -407,7 +408,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
.Wait(TimeSpan.FromSeconds(5))
|
||||
)
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
_logger.LogWarning(
|
||||
"Retention sweep: framework refresh timed out after 5s."
|
||||
);
|
||||
}
|
||||
@@ -420,7 +421,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
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);
|
||||
}
|
||||
finally
|
||||
@@ -568,7 +569,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.LogProxy.Error(e, "Failed to compute cleanup preview");
|
||||
_logger.LogError(e, "Failed to compute cleanup preview");
|
||||
WrapperUtil.AddNotification(
|
||||
HellionStrings.Cleanup_PreviewError,
|
||||
NotificationType.Error
|
||||
@@ -589,7 +590,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
try
|
||||
{
|
||||
var deleted = Plugin.MessageManager.Store.CleanupRetainOnly(allowed);
|
||||
Plugin.LogProxy.Information($"Privacy cleanup: deleted {deleted} messages");
|
||||
_logger.LogInformation($"Privacy cleanup: deleted {deleted} messages");
|
||||
|
||||
if (
|
||||
!Plugin
|
||||
@@ -601,9 +602,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
.Wait(TimeSpan.FromSeconds(5))
|
||||
)
|
||||
{
|
||||
Plugin.LogProxy.Warning(
|
||||
"Privacy cleanup: framework refresh timed out after 5s."
|
||||
);
|
||||
_logger.LogWarning("Privacy cleanup: framework refresh timed out after 5s.");
|
||||
}
|
||||
|
||||
WrapperUtil.AddNotification(
|
||||
@@ -613,7 +612,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.LogProxy.Error(e, "Privacy cleanup failed");
|
||||
_logger.LogError(e, "Privacy cleanup failed");
|
||||
WrapperUtil.AddNotification(HellionStrings.Cleanup_Error, NotificationType.Error);
|
||||
}
|
||||
finally
|
||||
@@ -773,7 +772,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Plugin.LogProxy.Error(e, "Export failed");
|
||||
_logger.LogError(e, "Export failed");
|
||||
WrapperUtil.AddNotification(HellionStrings.Export_Error, NotificationType.Error);
|
||||
}
|
||||
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.ClearAllTabs();
|
||||
|
||||
@@ -911,7 +910,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
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 playerName = Plugin.PlayerState.CharacterName;
|
||||
@@ -956,7 +955,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
var elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.LogProxy.Info(
|
||||
_logger.LogInformation(
|
||||
$"Crafted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
|
||||
@@ -966,7 +965,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.LogProxy.Info(
|
||||
_logger.LogInformation(
|
||||
$"Upserted {count} messages in {elapsedTicks} ticks ({elapsedTicks / TimeSpan.TicksPerMillisecond}ms)"
|
||||
);
|
||||
|
||||
@@ -977,7 +976,7 @@ internal sealed class DataManagement : ISettingsTab
|
||||
Plugin.MessageManager.ClearAllTabs();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.LogProxy.Info(
|
||||
_logger.LogInformation(
|
||||
$"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();
|
||||
elapsedTicks = stopwatch.ElapsedTicks;
|
||||
stopwatch.Stop();
|
||||
Plugin.LogProxy.Info(
|
||||
_logger.LogInformation(
|
||||
$"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.Resources;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Ui.SettingsTabs;
|
||||
|
||||
@@ -14,14 +15,16 @@ internal sealed class FontsAndColours : ISettingsTab
|
||||
{
|
||||
private Plugin Plugin { get; }
|
||||
private Configuration Mutable { get; }
|
||||
private readonly ILogger<FontsAndColours> _logger;
|
||||
|
||||
public string Name =>
|
||||
HellionStrings.Settings_Card_FontsAndColours_Title + "###tabs-fontsandcolours";
|
||||
|
||||
internal FontsAndColours(Plugin plugin, Configuration mutable)
|
||||
internal FontsAndColours(Plugin plugin, Configuration mutable, ILogger<FontsAndColours> logger)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Mutable = mutable;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
@@ -312,6 +315,6 @@ internal sealed class FontsAndColours : ISettingsTab
|
||||
}
|
||||
Plugin.SaveConfig();
|
||||
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.Themes;
|
||||
using HellionChat.Util;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace HellionChat.Ui.SettingsTabs;
|
||||
|
||||
@@ -11,16 +12,18 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
||||
{
|
||||
private Plugin Plugin { get; }
|
||||
private Configuration Mutable { get; }
|
||||
private readonly ILogger<ThemeAndLayout> _logger;
|
||||
|
||||
private string? _applyDismissedFor;
|
||||
|
||||
public string Name =>
|
||||
HellionStrings.Settings_Card_ThemeAndLayout_Title + "###tabs-themeandlayout";
|
||||
|
||||
internal ThemeAndLayout(Plugin plugin, Configuration mutable)
|
||||
internal ThemeAndLayout(Plugin plugin, Configuration mutable, ILogger<ThemeAndLayout> logger)
|
||||
{
|
||||
Plugin = plugin;
|
||||
Mutable = mutable;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Draw(bool changed)
|
||||
@@ -90,7 +93,7 @@ internal sealed class ThemeAndLayout : ISettingsTab
|
||||
var path = Path.Combine(dir, fileName);
|
||||
var json = ThemeJsonWriter.Serialize(active);
|
||||
File.WriteAllText(path, json);
|
||||
Plugin.LogProxy.Information($"Exported active theme '{active.Slug}' to {path}");
|
||||
_logger.LogInformation($"Exported active theme '{active.Slug}' to {path}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ using System;
|
||||
|
||||
namespace HellionChat.Util;
|
||||
|
||||
// Indirection over Dalamud's IPluginLog so MessageStore can be constructed
|
||||
// in an isolated xUnit AppDomain without loading Dalamud.dll — same pattern
|
||||
// as IPlatformUtil from F12.1. A later DI-container cycle (v1.5.x) may
|
||||
// replace this with Microsoft.Extensions.Logging's ILogger<T>.
|
||||
// Plugin.LogProxy bridge for consumers that cannot take a logger via the
|
||||
// constructor: static helpers (EmoteCache et al.), Dalamud-reflected types
|
||||
// (Configuration), data classes with mass instantiation (Message) and
|
||||
// instance classes that only log from static methods (FontManager).
|
||||
internal interface IPluginLogProxy
|
||||
{
|
||||
void Verbose(string message);
|
||||
|
||||
+395
-107
@@ -1,110 +1,398 @@
|
||||
{
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0-windows7.0": {
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[15.0.0, )",
|
||||
"resolved": "15.0.0",
|
||||
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
|
||||
},
|
||||
"DotNet.ReproducibleBuilds": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.39, )",
|
||||
"resolved": "1.2.39",
|
||||
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.4, 4.0.0)",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
||||
"dependencies": {
|
||||
"MessagePack.Annotations": "3.1.4",
|
||||
"MessagePackAnalyzer": "3.1.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"
|
||||
}
|
||||
}
|
||||
"version": 1,
|
||||
"dependencies": {
|
||||
"net10.0-windows7.0": {
|
||||
"DalamudPackager": {
|
||||
"type": "Direct",
|
||||
"requested": "[15.0.0, )",
|
||||
"resolved": "15.0.0",
|
||||
"contentHash": "411vwC8/X8Z/sQ2TI6v3SvOn66xFPeOjFn3Zn+h0d3Ox2t1kFm66AhDvmx/qcMwVrR+Hidxj0dadpQ2dgyXMBQ=="
|
||||
},
|
||||
"DotNet.ReproducibleBuilds": {
|
||||
"type": "Direct",
|
||||
"requested": "[1.2.39, )",
|
||||
"resolved": "1.2.39",
|
||||
"contentHash": "fcFN01tDTIQqDuTwr1jUQK/geofiwjG5DycJQOnC72i1SsLAk1ELe+apBOuZ11UMQG8YKFZG1FgvjZPbqHyatg=="
|
||||
},
|
||||
"MessagePack": {
|
||||
"type": "Direct",
|
||||
"requested": "[3.1.4, 4.0.0)",
|
||||
"resolved": "3.1.4",
|
||||
"contentHash": "BH0wlHWmVoZpbAPyyt2Awbq30C+ZsS3eHSkYdnyUAbqVJ22fAJDzn2xTieBeoT5QlcBzp61vHcv878YJGfi3mg==",
|
||||
"dependencies": {
|
||||
"MessagePack.Annotations": "3.1.4",
|
||||
"MessagePackAnalyzer": "3.1.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"
|
||||
}
|
||||
},
|
||||
"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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/actions/workflows/build.yml)
|
||||
[](LICENSE)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://gitea.hellion-forge.cloud/JonKazama-Hellion/HellionChat/releases/latest)
|
||||
[](https://github.com/goatcorp/Dalamud)
|
||||
[](https://dotnet.microsoft.com/)
|
||||
[](https://www.finalfantasyxiv.com/)
|
||||
@@ -11,7 +11,7 @@
|
||||
<img src="docs/images/hellion-forge.png" alt="Hellion Forge" width="180" />
|
||||
</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).
|
||||
|
||||
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
|
||||
|
||||
**Version 1.4.10** — Symbol-Picker and Tell-History Fix. Eleventh and final sub-patch of the v1.4.x polish sweep
|
||||
series. A new symbol-picker popup hangs off a smile-icon button left of the channel indicator: tab one lists all
|
||||
161 FFXIV PUA glyphs (Dalamud's `SeIconChar` enum); tab two carries 97 server-verified BMP symbols (latin marks,
|
||||
currency, the full Greek alphabet, geometric shapes, suits, notes) — each one round-tripped through `/echo` and
|
||||
`/say` in a four-round whitelist probe so the in-channel render matches what the picker shows. Click drops the
|
||||
glyph at the caret, multi-insert keeps the popup open, recent-used strip floats the last sixteen picks across
|
||||
both tabs. Toggle in Settings → Chat → Message behaviour, default on. Mid-cycle hotfix for pinned auto-tell tabs:
|
||||
PreloadHistory had a hidden 500-row SQL scan cap that overrode the user-configurable `AutoTellTabsHistoryPreload`
|
||||
setting — active users with many tell partners lost the backlog of less-frequent pinned partners. The cap is
|
||||
removed; the `(Receiver, Date)` index keeps SQL fast, the client-side loop respects the user setting as the upper
|
||||
bound. Slash-command teardown cleanup: `/hellion`, `/hellionView`, `/hellionDebugger` (and `#if DEBUG /hellionSeString`)
|
||||
wrappers are cached as private fields so plugin teardown detaches the live registration instead of re-Register'ing
|
||||
with identical args. The original Reserve-A `ImGuiListClipper` refactor for `DrawMessages` was cancelled after
|
||||
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).
|
||||
**Version 1.5.0** — DI Foundation and Service Refactor. Major architecture cycle: the plugin bootstrap moves to a
|
||||
generic-host DI container (`Microsoft.Extensions.Hosting` + `IServiceCollection`) modelled on Lightless Sync. All
|
||||
18 instance-class services migrate from a static `Plugin.LogProxy` locator to `Microsoft.Extensions.Logging.ILogger<T>`
|
||||
via constructor injection, with a custom `DalamudLogger` bridging the framework over to Dalamud's `IPluginLog`. The
|
||||
proxy stays for the eight buckets ctor-injection cannot reach (static helpers like `EmoteCache`, Dalamud-reflected
|
||||
`Configuration`, the `Message` data class, and static methods inside `FontManager` / `GameFunctions`). Plugin.cs
|
||||
finishes the cycle at 1012 lines — virtually identical to the pre-cycle 1013 — because the new Phase-1 host build
|
||||
and Plugin.X bridge wiring trade out exactly the service and window allocations that left `LoadAsync`. Cross-plugin
|
||||
baseline confirms no performance penalty vs Chat 2: HellionChat first-frame HITCH 77 ms median, Chat 2 74 ms.
|
||||
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. One user-visible fix bundled in from upstream: pasting a slash command
|
||||
into the chat input (Friend List "/tell" action, plugin-driven inserts) now replaces the existing input instead of
|
||||
concatenating onto whatever the user was typing. Migration v17 stays (no schema bump).
|
||||
|
||||
Hellion Chat is a standalone plugin, no longer a fork in the repository sense. Fully completed:
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
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
@@ -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`
|
||||
pattern. The v1.4.x Polish-Sweep series is closed; v1.5.0 starts the structural cycle that the smaller F12.x indirection
|
||||
shims (`IPluginLogProxy`, `IPlatformUtil`) were paving the way for. After that, the Wine/Linux scroll-rubber-band spike
|
||||
deferred from v1.4.10 (Reserve-A cancelled — Windows users never saw it) plus the First-Run-Wizard rework that lets users
|
||||
opt into the curated defaults instead of just picking a privacy profile.
|
||||
**Honorific Full Gradient Port plus FontAtlas-Defer for a 10× HITCH cut.** v1.5.0 closed the DI-container cycle with
|
||||
no performance penalty against Chat 2 (77 ms vs 74 ms median first-frame HITCH), but the cross-plugin baseline against
|
||||
Lightless Sync and XIVInstantMessenger surfaced a clean optimisation: both plugins defer their font-atlas build until
|
||||
after `Finished loading` and sit at 6-7 ms HITCH, an order of magnitude below the ~75 ms floor that Chat 2 and HellionChat
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user