Move migrations and service allocations from Phase-1 ctor to LoadAsync
Phase-1 was still doing 7 schema migrations and 25+ service allocations synchronously, blocking the ctor return. Move all of that to LoadAsync, keeping only bootstrap-essentials in the ctor: conflict detection, config load, language init, ImGui init, WindowSystem skeleton. Decouple the font task from the LoadAsync await — font-build runs fire-and-forget, so first frames render with Dalamud's default font until the Hellion-Exo2/NotoSans atlas rebuild completes (visible "font-pop"). Mirrors ChatTwo's pattern; the perceived-load win comes from "Finished loading" landing earlier, not from a faster atlas build.
This commit is contained in:
+379
-371
@@ -108,10 +108,11 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
public Plugin()
|
public Plugin()
|
||||||
{
|
{
|
||||||
// v1.4.3 Phase 1 — sync ctor: only work that doesn't touch
|
// Phase-1 ctor stays minimal: bootstrap-essentials only (conflict
|
||||||
// Theme/Font/MessageManager/UI-windows lives here. Async Phase 2
|
// gate, config load, language + ImGui init, WindowSystem skeleton).
|
||||||
// (LoadAsync) owns those. Hooks subscribe at the END of LoadAsync
|
// Schema migrations and every service / window allocation moved to
|
||||||
// so Dalamud's first Draw tick never sees null services (B1).
|
// LoadAsync so the sync ctor returns fast. On failure here nothing
|
||||||
|
// is initialized yet, so just throw — there is nothing to clean up.
|
||||||
|
|
||||||
// Refuse to start if upstream Chat 2 is loaded — prevents IPC
|
// Refuse to start if upstream Chat 2 is loaded — prevents IPC
|
||||||
// channel collisions and double-replacement of the in-game chat
|
// channel collisions and double-replacement of the in-game chat
|
||||||
@@ -134,345 +135,13 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
// Drop them on load to guarantee the session-only invariant.
|
// Drop them on load to guarantee the session-only invariant.
|
||||||
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
Config.Tabs.RemoveAll(t => t.IsTempTab);
|
||||||
|
|
||||||
// Hellion Chat v9 → v10 — wipes the configuration so the new 8-tab
|
|
||||||
// layout starts from defaults instead of mapping every previous setting
|
|
||||||
// to its new position. Backup-Failure ist non-fatal, der Wipe läuft
|
|
||||||
// trotzdem; dem User fehlt dann nur das manuelle Restore-Sicherheitsnetz.
|
|
||||||
if (Config.Version < 10)
|
|
||||||
{
|
|
||||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
|
||||||
if (pluginConfigsDir is not null)
|
|
||||||
{
|
|
||||||
var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
|
|
||||||
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v10-backup");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(liveConfigPath))
|
|
||||||
{
|
|
||||||
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Warning(ex, "HellionChat: pre-v10 config backup failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Config = new Configuration
|
|
||||||
{
|
|
||||||
Version = 10,
|
|
||||||
FirstRunCompleted = true,
|
|
||||||
};
|
|
||||||
SaveConfig();
|
|
||||||
|
|
||||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
|
||||||
{
|
|
||||||
Title = HellionStrings.SettingsRefactor_Migration_Title,
|
|
||||||
Content = HellionStrings.SettingsRefactor_Migration_Content,
|
|
||||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
|
||||||
InitialDuration = TimeSpan.FromSeconds(25),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hellion Chat v10 → v11 — adds the global Configuration.PopOutInputEnabled
|
|
||||||
// master switch and SeenPopOutInputHint flag for the v0.6.0 pop-out
|
|
||||||
// input feature. Lightweight migration: defaults both fields,
|
|
||||||
// no user-facing notification because the change is opt-in only.
|
|
||||||
if (Config.Version < 11)
|
|
||||||
{
|
|
||||||
Config.PopOutInputEnabled = false;
|
|
||||||
Config.SeenPopOutInputHint = false;
|
|
||||||
Config.Version = 11;
|
|
||||||
SaveConfig();
|
|
||||||
Log.Information(
|
|
||||||
"Migrated config v10 → v11: PopOutInputEnabled added (global, default off), " +
|
|
||||||
"SeenPopOutInputHint added (default false)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hellion Chat v11 → v12 — flips Configuration.PopOutInputEnabled from
|
|
||||||
// the v0.6.0 opt-in default (false) to opt-out (true) per v0.6.1 UX
|
|
||||||
// polish. Hard-flip is a deliberate design call (see Spec section 5.7);
|
|
||||||
// users are notified via the v0.6.1 hint banner (SeenPopOutHeaderHint
|
|
||||||
// reset). Re-toggle after migration is preserved because this block
|
|
||||||
// only fires for Version < 12.
|
|
||||||
if (Config.Version < 12)
|
|
||||||
{
|
|
||||||
Config.PopOutInputEnabled = true;
|
|
||||||
Config.SeenPopOutHeaderHint = false;
|
|
||||||
Config.Version = 12;
|
|
||||||
SaveConfig();
|
|
||||||
Log.Information(
|
|
||||||
"Migrated config v11 → v12: PopOutInputEnabled hard-flipped to true (v0.6.1 default), " +
|
|
||||||
"SeenPopOutHeaderHint reset to false (v0.6.1 banner re-armed)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hellion Chat v12 → v13 — hard-resets the tab layout to the
|
|
||||||
// sharpened v1.0.0 defaults (5 thematic tabs, see TabsUtil and
|
|
||||||
// the default-fill block below). Existing tab state is wiped
|
|
||||||
// because per-channel mapping from the old General preset to
|
|
||||||
// the new General/System split would be ambiguous and would
|
|
||||||
// produce subtly wrong results for users who tweaked the old
|
|
||||||
// layout. A timestamped backup of the live config is written
|
|
||||||
// alongside it as a manual restore safety net. The wipe scope
|
|
||||||
// is intentionally narrow: only Config.Tabs is reset; Privacy,
|
|
||||||
// Retention, Theme and every other knob keeps its current value.
|
|
||||||
if (Config.Version < 13)
|
|
||||||
{
|
|
||||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
|
||||||
if (pluginConfigsDir is not null)
|
|
||||||
{
|
|
||||||
var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
|
|
||||||
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v13-backup");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(liveConfigPath))
|
|
||||||
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Warning(ex, "HellionChat: pre-v13 config backup failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Config.Tabs.Clear();
|
|
||||||
Config.Version = 13;
|
|
||||||
SaveConfig();
|
|
||||||
|
|
||||||
Log.Information(
|
|
||||||
"Migrated config v12 → v13: tab layout hard-reset to v1.0.0 defaults; " +
|
|
||||||
"pre-v13 config backup written next to the live file. " +
|
|
||||||
"Default tabs will be populated by the Tabs.Count == 0 block.");
|
|
||||||
|
|
||||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
|
||||||
{
|
|
||||||
Title = HellionStrings.SettingsRefactor_Migration_Title,
|
|
||||||
Content = HellionStrings.SettingsRefactor_Migration_Content,
|
|
||||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
|
||||||
InitialDuration = TimeSpan.FromSeconds(25),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hellion Chat v13 → v14 — theme-engine migration. Alle User landen
|
|
||||||
// auf "hellion-arctic" als neues Default-Theme; die alte
|
|
||||||
// HellionThemeEnabled-Flag wird deprecated und nur noch ein Release
|
|
||||||
// als Safety-Net im JSON behalten. Window-Opacity wandert von
|
|
||||||
// HellionThemeWindowOpacity in das neue WindowOpacity-Feld.
|
|
||||||
//
|
|
||||||
// v1.4.0 (F5.4): Pre-v13-Backup wird gelesen, HellionThemeWindowOpacity
|
|
||||||
// ins neue Feld gezogen. Override nur wenn WindowOpacity noch beim
|
|
||||||
// Default sitzt — sonst hat der User in der Zwischenzeit (z.B. via
|
|
||||||
// WindowAlpha → WindowOpacity in v15→v16) explizit etwas gesetzt.
|
|
||||||
if (Config.Version < 14)
|
|
||||||
{
|
|
||||||
Config.Theme = "hellion-arctic";
|
|
||||||
|
|
||||||
var oldThemeOpacity = TryReadPreV13ThemeOpacity();
|
|
||||||
if (oldThemeOpacity is { } legacy
|
|
||||||
&& Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f)
|
|
||||||
{
|
|
||||||
Config.WindowOpacity = Math.Clamp(legacy, 0.5f, 1.0f);
|
|
||||||
Log.Information(
|
|
||||||
$"Migrated pre-v13 HellionThemeWindowOpacity {legacy} to WindowOpacity {Config.WindowOpacity}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Config.ReduceMotion = false;
|
|
||||||
Config.UseCompactDensity = false;
|
|
||||||
Config.Version = 14;
|
|
||||||
SaveConfig();
|
|
||||||
Log.Information(
|
|
||||||
"Migrated config v13 → v14: theme engine introduced, all users land on hellion-arctic; " +
|
|
||||||
"pick chat2-classic in Settings → Themes for the upstream look");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Config.Version < 15)
|
|
||||||
{
|
|
||||||
// v1.2.0 — keine Datenmigration nötig. Removal der deprecated
|
|
||||||
// Theme-Felder ist reine Schema-Bereinigung (System.Text.Json
|
|
||||||
// ignoriert unbekannte Felder im JSON, daher kein Crash bei
|
|
||||||
// Configs die noch HellionThemeEnabled/HellionThemeWindowOpacity
|
|
||||||
// serialisiert haben — die Werte verfallen einfach).
|
|
||||||
Config.Version = 15;
|
|
||||||
SaveConfig();
|
|
||||||
Log.Information(
|
|
||||||
"Migrated config v14 → v15: legacy theme fields removed " +
|
|
||||||
"(HellionThemeEnabled, HellionThemeWindowOpacity)");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hellion Chat v15 → v16 — Settings Cleanup. Re-Sortierung der
|
|
||||||
// Tabs auf der UI-Seite (datenneutral). 4 tote Felder verfallen
|
|
||||||
// beim System.Text.Json-Deserialize (OverrideStyle, ChosenStyle,
|
|
||||||
// WindowAlpha, ShowThemeQuickPicker — sind alle nicht mehr im
|
|
||||||
// Configuration-Schema definiert). WindowAlpha wird zuvor auf
|
|
||||||
// WindowOpacity gemappt damit User die ihn gesetzt hatten ihre
|
|
||||||
// Transparenz-Einstellung behalten.
|
|
||||||
if (Config.Version < 16)
|
|
||||||
{
|
|
||||||
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
|
||||||
var liveConfigPath = pluginConfigsDir is not null
|
|
||||||
? Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json")
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Backup-Datei neben der live Config — Pattern aus v13 Branch.
|
|
||||||
if (pluginConfigsDir is not null && liveConfigPath is not null)
|
|
||||||
{
|
|
||||||
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v16-backup");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(liveConfigPath))
|
|
||||||
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Warning(ex, "HellionChat: pre-v16 config backup failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-v16 Felder einmalig roh aus dem JSON lesen, da sie nicht
|
|
||||||
// mehr im Configuration-Schema sind (und damit aus Config nicht
|
|
||||||
// mehr abrufbar). WindowAlpha → WindowOpacity Mapping nur wenn
|
|
||||||
// User WindowOpacity noch nicht selbst angefasst hat (Default
|
|
||||||
// 0.85), sonst gewinnt der User-Wert.
|
|
||||||
float oldWindowAlpha = 100f;
|
|
||||||
bool oldOverrideStyle = false;
|
|
||||||
if (liveConfigPath is not null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (File.Exists(liveConfigPath))
|
|
||||||
{
|
|
||||||
var rawJson = File.ReadAllText(liveConfigPath);
|
|
||||||
using var doc = System.Text.Json.JsonDocument.Parse(rawJson);
|
|
||||||
if (doc.RootElement.TryGetProperty("WindowAlpha", out var alphaProp)
|
|
||||||
&& alphaProp.ValueKind == System.Text.Json.JsonValueKind.Number)
|
|
||||||
{
|
|
||||||
oldWindowAlpha = alphaProp.GetSingle();
|
|
||||||
}
|
|
||||||
if (doc.RootElement.TryGetProperty("OverrideStyle", out var ovProp)
|
|
||||||
&& ovProp.ValueKind is System.Text.Json.JsonValueKind.True)
|
|
||||||
{
|
|
||||||
oldOverrideStyle = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log.Warning(ex, "HellionChat: pre-v16 legacy-field lookup failed, defaults assumed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TEST-MIRROR: Hellion Build test/_Helpers/MigrationLogic.cs (local-only repo)
|
|
||||||
// If you change the formula here, change the mirror — tests assert the contract.
|
|
||||||
if (oldWindowAlpha != 100f
|
|
||||||
&& Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f)
|
|
||||||
{
|
|
||||||
Config.WindowOpacity = Math.Clamp(oldWindowAlpha / 100f, 0.5f, 1.0f);
|
|
||||||
Log.Information(
|
|
||||||
$"Migrated WindowAlpha {oldWindowAlpha} to WindowOpacity {Config.WindowOpacity}");
|
|
||||||
}
|
|
||||||
else if (oldWindowAlpha != 100f)
|
|
||||||
{
|
|
||||||
Log.Information(
|
|
||||||
$"Skipped WindowAlpha→WindowOpacity migration: WindowOpacity already user-set " +
|
|
||||||
$"({Config.WindowOpacity}), legacy WindowAlpha value {oldWindowAlpha} dropped.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldOverrideStyle)
|
|
||||||
{
|
|
||||||
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
|
||||||
{
|
|
||||||
Title = "Hellion Chat 1.2.1",
|
|
||||||
Content = HellionStrings.Migration_v16_OverrideStyle_Toast,
|
|
||||||
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
|
||||||
InitialDuration = TimeSpan.FromSeconds(25),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// v1.2.1 Default-Bumps für UX-Verbesserungen. Pattern: nur
|
|
||||||
// migrieren wenn der User noch auf dem alten Default ist.
|
|
||||||
// Bei bool-Werten ist die Erkennung pragmatisch — wer den
|
|
||||||
// alten Default aktiv ausgeschaltet hatte, erlebt das als
|
|
||||||
// Regression und stellt es einmal in den Settings zurück.
|
|
||||||
// Der Trade-Off ist akzeptabel weil die alten Defaults in
|
|
||||||
// v1.2.0 erst neu eingeführt wurden und kaum jemand aktiv
|
|
||||||
// umgeschaltet hat.
|
|
||||||
if (!Config.UseCompactDensity)
|
|
||||||
{
|
|
||||||
Config.UseCompactDensity = true;
|
|
||||||
Log.Information("v16 default-bump: UseCompactDensity false → true");
|
|
||||||
}
|
|
||||||
if (!Config.HideInNewGamePlusMenu)
|
|
||||||
{
|
|
||||||
Config.HideInNewGamePlusMenu = true;
|
|
||||||
Log.Information("v16 default-bump: HideInNewGamePlusMenu false → true");
|
|
||||||
}
|
|
||||||
if (!Config.HideSameTimestamps)
|
|
||||||
{
|
|
||||||
Config.HideSameTimestamps = true;
|
|
||||||
Log.Information("v16 default-bump: HideSameTimestamps false → true");
|
|
||||||
}
|
|
||||||
if (Config.MaxLinesToRender == 5000)
|
|
||||||
{
|
|
||||||
Config.MaxLinesToRender = 2500;
|
|
||||||
Log.Information("v16 default-bump: MaxLinesToRender 5000 → 2500");
|
|
||||||
}
|
|
||||||
if (Config.ChatColours.Count == 0)
|
|
||||||
{
|
|
||||||
foreach (var (channel, colour) in Resources.ChatColourPresets.All["Hellion"].Colours)
|
|
||||||
Config.ChatColours[channel] = colour;
|
|
||||||
Log.Information("v16 default-bump: ChatColours empty → Hellion brand preset");
|
|
||||||
}
|
|
||||||
|
|
||||||
Config.Version = 16;
|
|
||||||
SaveConfig();
|
|
||||||
Log.Information(
|
|
||||||
"Migrated config v15 → v16: settings cleanup, " +
|
|
||||||
"OverrideStyle/ChosenStyle/WindowAlpha/ShowThemeQuickPicker dropped from schema");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hellion v1.0.0 default tab layout. Five thematically separated
|
|
||||||
// tabs: General catches the immediate-surroundings public chat
|
|
||||||
// (Say/Yell/Shout) only; System absorbs the rest of the technical
|
|
||||||
// and gameplay-event noise; FreeCompany, Group and Linkshell each
|
|
||||||
// own their respective channel set. Tells are not in a static
|
|
||||||
// tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation
|
|
||||||
// tabs on demand. Novice-Network gets no preset tab; users who
|
|
||||||
// want it can add HellionBeginner from Settings → Tabs.
|
|
||||||
if (Config.Tabs.Count == 0)
|
|
||||||
{
|
|
||||||
Config.Tabs.Add(TabsUtil.VanillaGeneral);
|
|
||||||
Config.Tabs.Add(TabsUtil.HellionSystem);
|
|
||||||
Config.Tabs.Add(TabsUtil.HellionFreeCompany);
|
|
||||||
Config.Tabs.Add(TabsUtil.HellionParty);
|
|
||||||
Config.Tabs.Add(TabsUtil.HellionLinkshell);
|
|
||||||
}
|
|
||||||
|
|
||||||
LanguageChanged(Interface.UiLanguage);
|
LanguageChanged(Interface.UiLanguage);
|
||||||
ImGuiUtil.Initialize(this);
|
ImGuiUtil.Initialize(this);
|
||||||
|
|
||||||
FileDialogManager = new FileDialogManager();
|
DeferredSaveFrames = -1;
|
||||||
|
|
||||||
// Phase-1 services: pure allocation, no Theme/Font/UI touch.
|
// WindowSystem skeleton is initialised by the readonly field above —
|
||||||
Commands = new Commands();
|
// no AddWindow yet; window construction lives in LoadAsync.
|
||||||
Functions = new GameFunctions.GameFunctions(this);
|
|
||||||
Ipc = new IpcManager();
|
|
||||||
TypingIpc = new TypingIpc(this);
|
|
||||||
ExtraChat = new ExtraChat();
|
|
||||||
|
|
||||||
// Plugin integrations register their IPC subscribers up-front so
|
|
||||||
// Ready/Disposing events from the target plugins are caught from
|
|
||||||
// the very first frame, even if the user's Honorific reloads
|
|
||||||
// mid-session. See HellionChat/Integrations/HonorificService.cs.
|
|
||||||
HonorificService = new Integrations.HonorificService(Interface, Log, Framework);
|
|
||||||
|
|
||||||
StatusBar = new Ui.StatusBar();
|
|
||||||
|
|
||||||
Interface.UiBuilder.DisableCutsceneUiHide = true;
|
|
||||||
Interface.UiBuilder.DisableGposeUiHide = true;
|
|
||||||
|
|
||||||
if (Config.ShowEmotes)
|
|
||||||
_ = EmoteCache.LoadData(); // Fire-and-forget intentional, exceptions are caught inside
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task LoadAsync(CancellationToken cancellationToken)
|
public async Task LoadAsync(CancellationToken cancellationToken)
|
||||||
@@ -481,50 +150,377 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Group A: Font + Theme parallel — both CPU-bound, independent, and
|
// Schema migrations live in LoadAsync so the sync ctor returns fast.
|
||||||
// dominate the load-time profile. Everything else stays sequential to
|
// Each block is self-contained and runs sequentially before any
|
||||||
// keep ordering simple.
|
// service consumes Config; order matters because later versions
|
||||||
var fontTask = Task.Run(async () =>
|
// build on earlier ones (e.g. v16 reads opacity that v14 wrote).
|
||||||
|
|
||||||
|
// Hellion Chat v9 → v10 — wipes the configuration so the new 8-tab
|
||||||
|
// layout starts from defaults instead of mapping every previous setting
|
||||||
|
// to its new position. Backup-Failure ist non-fatal, der Wipe läuft
|
||||||
|
// trotzdem; dem User fehlt dann nur das manuelle Restore-Sicherheitsnetz.
|
||||||
|
if (Config.Version < 10)
|
||||||
|
{
|
||||||
|
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||||
|
if (pluginConfigsDir is not null)
|
||||||
|
{
|
||||||
|
var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
|
||||||
|
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v10-backup");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(liveConfigPath))
|
||||||
|
{
|
||||||
|
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "HellionChat: pre-v10 config backup failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Config = new Configuration
|
||||||
|
{
|
||||||
|
Version = 10,
|
||||||
|
FirstRunCompleted = true,
|
||||||
|
};
|
||||||
|
SaveConfig();
|
||||||
|
|
||||||
|
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||||
|
{
|
||||||
|
Title = HellionStrings.SettingsRefactor_Migration_Title,
|
||||||
|
Content = HellionStrings.SettingsRefactor_Migration_Content,
|
||||||
|
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||||
|
InitialDuration = TimeSpan.FromSeconds(25),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v10 → v11 — adds the global Configuration.PopOutInputEnabled
|
||||||
|
// master switch and SeenPopOutInputHint flag for the v0.6.0 pop-out
|
||||||
|
// input feature. Lightweight migration: defaults both fields,
|
||||||
|
// no user-facing notification because the change is opt-in only.
|
||||||
|
if (Config.Version < 11)
|
||||||
|
{
|
||||||
|
Config.PopOutInputEnabled = false;
|
||||||
|
Config.SeenPopOutInputHint = false;
|
||||||
|
Config.Version = 11;
|
||||||
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v10 → v11: PopOutInputEnabled added (global, default off), " +
|
||||||
|
"SeenPopOutInputHint added (default false)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v11 → v12 — flips Configuration.PopOutInputEnabled from
|
||||||
|
// the v0.6.0 opt-in default (false) to opt-out (true) per v0.6.1 UX
|
||||||
|
// polish. Hard-flip is a deliberate design call (see Spec section 5.7);
|
||||||
|
// users are notified via the v0.6.1 hint banner (SeenPopOutHeaderHint
|
||||||
|
// reset). Re-toggle after migration is preserved because this block
|
||||||
|
// only fires for Version < 12.
|
||||||
|
if (Config.Version < 12)
|
||||||
|
{
|
||||||
|
Config.PopOutInputEnabled = true;
|
||||||
|
Config.SeenPopOutHeaderHint = false;
|
||||||
|
Config.Version = 12;
|
||||||
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v11 → v12: PopOutInputEnabled hard-flipped to true (v0.6.1 default), " +
|
||||||
|
"SeenPopOutHeaderHint reset to false (v0.6.1 banner re-armed)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v12 → v13 — hard-resets the tab layout to the
|
||||||
|
// sharpened v1.0.0 defaults (5 thematic tabs, see TabsUtil and
|
||||||
|
// the default-fill block below). Existing tab state is wiped
|
||||||
|
// because per-channel mapping from the old General preset to
|
||||||
|
// the new General/System split would be ambiguous and would
|
||||||
|
// produce subtly wrong results for users who tweaked the old
|
||||||
|
// layout. A timestamped backup of the live config is written
|
||||||
|
// alongside it as a manual restore safety net. The wipe scope
|
||||||
|
// is intentionally narrow: only Config.Tabs is reset; Privacy,
|
||||||
|
// Retention, Theme and every other knob keeps its current value.
|
||||||
|
if (Config.Version < 13)
|
||||||
|
{
|
||||||
|
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||||
|
if (pluginConfigsDir is not null)
|
||||||
|
{
|
||||||
|
var liveConfigPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json");
|
||||||
|
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v13-backup");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(liveConfigPath))
|
||||||
|
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "HellionChat: pre-v13 config backup failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.Tabs.Clear();
|
||||||
|
Config.Version = 13;
|
||||||
|
SaveConfig();
|
||||||
|
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v12 → v13: tab layout hard-reset to v1.0.0 defaults; " +
|
||||||
|
"pre-v13 config backup written next to the live file. " +
|
||||||
|
"Default tabs will be populated by the Tabs.Count == 0 block.");
|
||||||
|
|
||||||
|
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||||
|
{
|
||||||
|
Title = HellionStrings.SettingsRefactor_Migration_Title,
|
||||||
|
Content = HellionStrings.SettingsRefactor_Migration_Content,
|
||||||
|
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||||
|
InitialDuration = TimeSpan.FromSeconds(25),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v13 → v14 — theme-engine migration. Alle User landen
|
||||||
|
// auf "hellion-arctic" als neues Default-Theme; die alte
|
||||||
|
// HellionThemeEnabled-Flag wird deprecated und nur noch ein Release
|
||||||
|
// als Safety-Net im JSON behalten. Window-Opacity wandert von
|
||||||
|
// HellionThemeWindowOpacity in das neue WindowOpacity-Feld.
|
||||||
|
//
|
||||||
|
// v1.4.0 (F5.4): Pre-v13-Backup wird gelesen, HellionThemeWindowOpacity
|
||||||
|
// ins neue Feld gezogen. Override nur wenn WindowOpacity noch beim
|
||||||
|
// Default sitzt — sonst hat der User in der Zwischenzeit (z.B. via
|
||||||
|
// WindowAlpha → WindowOpacity in v15→v16) explizit etwas gesetzt.
|
||||||
|
if (Config.Version < 14)
|
||||||
|
{
|
||||||
|
Config.Theme = "hellion-arctic";
|
||||||
|
|
||||||
|
var oldThemeOpacity = TryReadPreV13ThemeOpacity();
|
||||||
|
if (oldThemeOpacity is { } legacy
|
||||||
|
&& Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f)
|
||||||
|
{
|
||||||
|
Config.WindowOpacity = Math.Clamp(legacy, 0.5f, 1.0f);
|
||||||
|
Log.Information(
|
||||||
|
$"Migrated pre-v13 HellionThemeWindowOpacity {legacy} to WindowOpacity {Config.WindowOpacity}");
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.ReduceMotion = false;
|
||||||
|
Config.UseCompactDensity = false;
|
||||||
|
Config.Version = 14;
|
||||||
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v13 → v14: theme engine introduced, all users land on hellion-arctic; " +
|
||||||
|
"pick chat2-classic in Settings → Themes for the upstream look");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Config.Version < 15)
|
||||||
|
{
|
||||||
|
// v1.2.0 — keine Datenmigration nötig. Removal der deprecated
|
||||||
|
// Theme-Felder ist reine Schema-Bereinigung (System.Text.Json
|
||||||
|
// ignoriert unbekannte Felder im JSON, daher kein Crash bei
|
||||||
|
// Configs die noch HellionThemeEnabled/HellionThemeWindowOpacity
|
||||||
|
// serialisiert haben — die Werte verfallen einfach).
|
||||||
|
Config.Version = 15;
|
||||||
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v14 → v15: legacy theme fields removed " +
|
||||||
|
"(HellionThemeEnabled, HellionThemeWindowOpacity)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hellion Chat v15 → v16 — Settings Cleanup. Re-Sortierung der
|
||||||
|
// Tabs auf der UI-Seite (datenneutral). 4 tote Felder verfallen
|
||||||
|
// beim System.Text.Json-Deserialize (OverrideStyle, ChosenStyle,
|
||||||
|
// WindowAlpha, ShowThemeQuickPicker — sind alle nicht mehr im
|
||||||
|
// Configuration-Schema definiert). WindowAlpha wird zuvor auf
|
||||||
|
// WindowOpacity gemappt damit User die ihn gesetzt hatten ihre
|
||||||
|
// Transparenz-Einstellung behalten.
|
||||||
|
if (Config.Version < 16)
|
||||||
|
{
|
||||||
|
var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName;
|
||||||
|
var liveConfigPath = pluginConfigsDir is not null
|
||||||
|
? Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json")
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Backup-Datei neben der live Config — Pattern aus v13 Branch.
|
||||||
|
if (pluginConfigsDir is not null && liveConfigPath is not null)
|
||||||
|
{
|
||||||
|
var backupPath = Path.Combine(pluginConfigsDir, $"{Interface.InternalName}.json.pre-v16-backup");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(liveConfigPath))
|
||||||
|
File.Copy(liveConfigPath, backupPath, overwrite: true);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "HellionChat: pre-v16 config backup failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-v16 Felder einmalig roh aus dem JSON lesen, da sie nicht
|
||||||
|
// mehr im Configuration-Schema sind (und damit aus Config nicht
|
||||||
|
// mehr abrufbar). WindowAlpha → WindowOpacity Mapping nur wenn
|
||||||
|
// User WindowOpacity noch nicht selbst angefasst hat (Default
|
||||||
|
// 0.85), sonst gewinnt der User-Wert.
|
||||||
|
float oldWindowAlpha = 100f;
|
||||||
|
bool oldOverrideStyle = false;
|
||||||
|
if (liveConfigPath is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(liveConfigPath))
|
||||||
|
{
|
||||||
|
var rawJson = File.ReadAllText(liveConfigPath);
|
||||||
|
using var doc = System.Text.Json.JsonDocument.Parse(rawJson);
|
||||||
|
if (doc.RootElement.TryGetProperty("WindowAlpha", out var alphaProp)
|
||||||
|
&& alphaProp.ValueKind == System.Text.Json.JsonValueKind.Number)
|
||||||
|
{
|
||||||
|
oldWindowAlpha = alphaProp.GetSingle();
|
||||||
|
}
|
||||||
|
if (doc.RootElement.TryGetProperty("OverrideStyle", out var ovProp)
|
||||||
|
&& ovProp.ValueKind is System.Text.Json.JsonValueKind.True)
|
||||||
|
{
|
||||||
|
oldOverrideStyle = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Log.Warning(ex, "HellionChat: pre-v16 legacy-field lookup failed, defaults assumed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TEST-MIRROR: Hellion Build test/_Helpers/MigrationLogic.cs (local-only repo)
|
||||||
|
// If you change the formula here, change the mirror — tests assert the contract.
|
||||||
|
if (oldWindowAlpha != 100f
|
||||||
|
&& Math.Abs(Config.WindowOpacity - 0.85f) < 0.001f)
|
||||||
|
{
|
||||||
|
Config.WindowOpacity = Math.Clamp(oldWindowAlpha / 100f, 0.5f, 1.0f);
|
||||||
|
Log.Information(
|
||||||
|
$"Migrated WindowAlpha {oldWindowAlpha} to WindowOpacity {Config.WindowOpacity}");
|
||||||
|
}
|
||||||
|
else if (oldWindowAlpha != 100f)
|
||||||
|
{
|
||||||
|
Log.Information(
|
||||||
|
$"Skipped WindowAlpha→WindowOpacity migration: WindowOpacity already user-set " +
|
||||||
|
$"({Config.WindowOpacity}), legacy WindowAlpha value {oldWindowAlpha} dropped.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldOverrideStyle)
|
||||||
|
{
|
||||||
|
Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification
|
||||||
|
{
|
||||||
|
Title = "Hellion Chat 1.2.1",
|
||||||
|
Content = HellionStrings.Migration_v16_OverrideStyle_Toast,
|
||||||
|
Type = Dalamud.Interface.ImGuiNotification.NotificationType.Info,
|
||||||
|
InitialDuration = TimeSpan.FromSeconds(25),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// v1.2.1 Default-Bumps für UX-Verbesserungen. Pattern: nur
|
||||||
|
// migrieren wenn der User noch auf dem alten Default ist.
|
||||||
|
// Bei bool-Werten ist die Erkennung pragmatisch — wer den
|
||||||
|
// alten Default aktiv ausgeschaltet hatte, erlebt das als
|
||||||
|
// Regression und stellt es einmal in den Settings zurück.
|
||||||
|
// Der Trade-Off ist akzeptabel weil die alten Defaults in
|
||||||
|
// v1.2.0 erst neu eingeführt wurden und kaum jemand aktiv
|
||||||
|
// umgeschaltet hat.
|
||||||
|
if (!Config.UseCompactDensity)
|
||||||
|
{
|
||||||
|
Config.UseCompactDensity = true;
|
||||||
|
Log.Information("v16 default-bump: UseCompactDensity false → true");
|
||||||
|
}
|
||||||
|
if (!Config.HideInNewGamePlusMenu)
|
||||||
|
{
|
||||||
|
Config.HideInNewGamePlusMenu = true;
|
||||||
|
Log.Information("v16 default-bump: HideInNewGamePlusMenu false → true");
|
||||||
|
}
|
||||||
|
if (!Config.HideSameTimestamps)
|
||||||
|
{
|
||||||
|
Config.HideSameTimestamps = true;
|
||||||
|
Log.Information("v16 default-bump: HideSameTimestamps false → true");
|
||||||
|
}
|
||||||
|
if (Config.MaxLinesToRender == 5000)
|
||||||
|
{
|
||||||
|
Config.MaxLinesToRender = 2500;
|
||||||
|
Log.Information("v16 default-bump: MaxLinesToRender 5000 → 2500");
|
||||||
|
}
|
||||||
|
if (Config.ChatColours.Count == 0)
|
||||||
|
{
|
||||||
|
foreach (var (channel, colour) in Resources.ChatColourPresets.All["Hellion"].Colours)
|
||||||
|
Config.ChatColours[channel] = colour;
|
||||||
|
Log.Information("v16 default-bump: ChatColours empty → Hellion brand preset");
|
||||||
|
}
|
||||||
|
|
||||||
|
Config.Version = 16;
|
||||||
|
SaveConfig();
|
||||||
|
Log.Information(
|
||||||
|
"Migrated config v15 → v16: settings cleanup, " +
|
||||||
|
"OverrideStyle/ChosenStyle/WindowAlpha/ShowThemeQuickPicker dropped from schema");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hellion v1.0.0 default tab layout. Five thematically separated
|
||||||
|
// tabs: General catches the immediate-surroundings public chat
|
||||||
|
// (Say/Yell/Shout) only; System absorbs the rest of the technical
|
||||||
|
// and gameplay-event noise; FreeCompany, Group and Linkshell each
|
||||||
|
// own their respective channel set. Tells are not in a static
|
||||||
|
// tab anymore — Auto-Tell-Tabs spawns dedicated per-conversation
|
||||||
|
// tabs on demand. Novice-Network gets no preset tab; users who
|
||||||
|
// want it can add HellionBeginner from Settings → Tabs.
|
||||||
|
if (Config.Tabs.Count == 0)
|
||||||
|
{
|
||||||
|
Config.Tabs.Add(TabsUtil.VanillaGeneral);
|
||||||
|
Config.Tabs.Add(TabsUtil.HellionSystem);
|
||||||
|
Config.Tabs.Add(TabsUtil.HellionFreeCompany);
|
||||||
|
Config.Tabs.Add(TabsUtil.HellionParty);
|
||||||
|
Config.Tabs.Add(TabsUtil.HellionLinkshell);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
// Font-build runs fire-and-forget. The atlas rebuild lands a few
|
||||||
|
// hundred ms after LoadAsync returns; first frames draw with
|
||||||
|
// Dalamud's default font until the Hellion-Exo2 / NotoSans handles
|
||||||
|
// are ready, then ImGui switches to the custom fonts (visible
|
||||||
|
// "font-pop"). Mirrors ChatTwo's pattern — perceived-load win
|
||||||
|
// comes from "Finished loading" landing earlier, not from a faster
|
||||||
|
// atlas build.
|
||||||
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
FontManager = new FontManager();
|
FontManager = new FontManager();
|
||||||
await FontManager.BuildFontsAsync(cancellationToken).ConfigureAwait(false);
|
await FontManager.BuildFontsAsync(cancellationToken).ConfigureAwait(false);
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
var themeTask = Task.Run(() =>
|
// Theme init stays sync on the LoadAsync continuation — cheap,
|
||||||
{
|
// and Active is read every Draw frame, so the registry must be
|
||||||
// v1.1.0 — Theme-Engine init. Custom-Themes liegen in
|
// wired before the first hook fires.
|
||||||
// pluginConfigs/HellionChat/themes/, lazy geladen beim ersten Get.
|
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
||||||
var customThemesDir = Path.Combine(Interface.ConfigDirectory.FullName, "themes");
|
Directory.CreateDirectory(customThemesDir);
|
||||||
Directory.CreateDirectory(customThemesDir);
|
SeedExampleThemeIfEmpty(customThemesDir);
|
||||||
SeedExampleThemeIfEmpty(customThemesDir);
|
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
|
||||||
ThemeRegistry = new Themes.ThemeRegistry(customThemesDir);
|
ThemeRegistry.Switch(Config.Theme);
|
||||||
ThemeRegistry.Switch(Config.Theme);
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
await Task.WhenAll(fontTask, themeTask).ConfigureAwait(false);
|
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// SelfTest hooks live alongside the live registry — the steps
|
// Service allocations: order encodes dependencies. Commands is
|
||||||
// poll Active per frame and need the registry already wired.
|
// alloc-only here; Initialise() runs after windows exist so the
|
||||||
|
// slash-commands can toggle their visibility. HonorificService
|
||||||
|
// registers IPC subscribers up-front so Ready/Disposing events
|
||||||
|
// are caught from the very 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);
|
||||||
|
|
||||||
|
// Auto-Tell-Tabs subscribes to MessageManager.MessageProcessed for
|
||||||
|
// live tells and to ClientState.Logout for cleanup; needs the live
|
||||||
|
// store handed in at construction.
|
||||||
|
AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store);
|
||||||
|
AutoTellTabsService.Initialize();
|
||||||
|
|
||||||
|
// SelfTest steps poll Active per frame and need the registry wired.
|
||||||
SelfTestRegistry.RegisterTestSteps([
|
SelfTestRegistry.RegisterTestSteps([
|
||||||
new SelfTests.ThemeSwitchSelfTestStep(this),
|
new SelfTests.ThemeSwitchSelfTestStep(this),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
MessageManager = new MessageManager(this);
|
|
||||||
|
|
||||||
// Daily retention sweep, fire-and-forget. Lives in Phase 2 because
|
|
||||||
// it dereferences MessageManager.Store. Skips itself when disabled
|
|
||||||
// or when it already ran within the past 24 hours.
|
|
||||||
RunRetentionSweepIfDue();
|
|
||||||
|
|
||||||
// Hellion Chat — Auto-Tell-Tabs service. Subscribes to the
|
|
||||||
// MessageManager's MessageProcessed event for live tells and
|
|
||||||
// to ClientState.Logout for the cleanup pass. Created after
|
|
||||||
// MessageManager so the constructor can hand off the live
|
|
||||||
// store and event source.
|
|
||||||
AutoTellTabsService = new AutoTellTabsService(this, MessageManager, MessageManager.Store);
|
|
||||||
AutoTellTabsService.Initialize();
|
|
||||||
|
|
||||||
ChatLogWindow = new ChatLogWindow(this);
|
ChatLogWindow = new ChatLogWindow(this);
|
||||||
SettingsWindow = new SettingsWindow(this);
|
SettingsWindow = new SettingsWindow(this);
|
||||||
DbViewer = new DbViewer(this);
|
DbViewer = new DbViewer(this);
|
||||||
@@ -548,12 +544,24 @@ public sealed class Plugin : IAsyncDalamudPlugin
|
|||||||
if (!Config.FirstRunCompleted)
|
if (!Config.FirstRunCompleted)
|
||||||
FirstRunWizard.IsOpen = true;
|
FirstRunWizard.IsOpen = true;
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
// let all the other components register, then initialize commands
|
// let all the other components register, then initialize commands
|
||||||
Commands.Initialise();
|
Commands.Initialise();
|
||||||
|
|
||||||
|
// Daily retention sweep, fire-and-forget. Skips itself when
|
||||||
|
// disabled or when it already ran within the past 24 hours.
|
||||||
|
RunRetentionSweepIfDue();
|
||||||
|
|
||||||
|
if (Config.ShowEmotes)
|
||||||
|
_ = EmoteCache.LoadData(); // Fire-and-forget, exceptions caught inside
|
||||||
|
|
||||||
if (Interface.Reason is not PluginLoadReason.Boot)
|
if (Interface.Reason is not PluginLoadReason.Boot)
|
||||||
MessageManager.FilterAllTabsAsync();
|
MessageManager.FilterAllTabsAsync();
|
||||||
|
|
||||||
|
Interface.UiBuilder.DisableCutsceneUiHide = true;
|
||||||
|
Interface.UiBuilder.DisableGposeUiHide = true;
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
// Avoid 300ms hitch when sending first message by preloading the
|
// Avoid 300ms hitch when sending first message by preloading the
|
||||||
// auto-translate cache. Don't do this in debug because it makes
|
// auto-translate cache. Don't do this in debug because it makes
|
||||||
|
|||||||
Reference in New Issue
Block a user