diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index 040d2b8..ca2d845 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -272,61 +272,108 @@ public sealed class Plugin : IDalamudPlugin private static void MigrateFromChatTwoLayout() { + var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; + if (pluginConfigsDir is null) + return; + + var legacyConfigFile = Path.Combine(pluginConfigsDir, "ChatTwo.json"); + var legacyConfigDir = Path.Combine(pluginConfigsDir, "ChatTwo"); + var ourConfigFile = Path.Combine(pluginConfigsDir, "HellionChat.json"); + var ourConfigDir = Interface.ConfigDirectory.FullName; + + // Track whether anything legitimately blocked us. The most common + // cause is upstream Chat 2 still being loaded — its SQLite handle + // keeps chat-sqlite.db locked and File.Move throws IOException. + var lockedBlocker = false; + try { - var pluginConfigsDir = Interface.ConfigDirectory.Parent?.FullName; - if (pluginConfigsDir is null) - return; - - var legacyConfigFile = Path.Combine(pluginConfigsDir, "ChatTwo.json"); - var legacyConfigDir = Path.Combine(pluginConfigsDir, "ChatTwo"); - var ourConfigFile = Path.Combine(pluginConfigsDir, "HellionChat.json"); - var ourConfigDir = Interface.ConfigDirectory.FullName; - if (!File.Exists(ourConfigFile) && File.Exists(legacyConfigFile)) { File.Move(legacyConfigFile, ourConfigFile); Log.Information($"HellionChat: migrated config file {legacyConfigFile} → {ourConfigFile}"); } + } + catch (IOException e) + { + Log.Warning(e, $"HellionChat: config file move blocked, leaving {legacyConfigFile} in place"); + lockedBlocker = true; + } - // The plugin's ConfigDirectory may already exist on first load - // (Dalamud creates it), so check at the file level instead of - // skipping when the directory is present. Move every legacy - // entry whose target name is not occupied yet, then remove the - // source dir if it ends up empty. - if (Directory.Exists(legacyConfigDir)) + // The plugin's ConfigDirectory may already exist on first load + // (Dalamud creates it), so check at the file level instead of + // skipping when the directory is present. Move every legacy + // entry whose target name is not occupied yet, then remove the + // source dir if it ends up empty. Each move is wrapped on its + // own so a single locked file (the SQLite db while ChatTwo still + // runs) does not abandon the rest of the migration. + if (!Directory.Exists(legacyConfigDir)) + return; + + try + { + Directory.CreateDirectory(ourConfigDir); + + foreach (var file in Directory.EnumerateFiles(legacyConfigDir)) { - Directory.CreateDirectory(ourConfigDir); - - foreach (var file in Directory.EnumerateFiles(legacyConfigDir)) + var target = Path.Combine(ourConfigDir, Path.GetFileName(file)); + if (File.Exists(target)) + continue; + try { - var target = Path.Combine(ourConfigDir, Path.GetFileName(file)); - if (File.Exists(target)) - continue; File.Move(file, target); Log.Information($"HellionChat: migrated file {file} → {target}"); } - - foreach (var dir in Directory.EnumerateDirectories(legacyConfigDir)) + catch (IOException e) + { + Log.Warning(e, $"HellionChat: file move blocked for {file}, will retry on next load"); + lockedBlocker = true; + } + } + + foreach (var dir in Directory.EnumerateDirectories(legacyConfigDir)) + { + var target = Path.Combine(ourConfigDir, Path.GetFileName(dir)); + if (Directory.Exists(target)) + continue; + try { - var target = Path.Combine(ourConfigDir, Path.GetFileName(dir)); - if (Directory.Exists(target)) - continue; Directory.Move(dir, target); Log.Information($"HellionChat: migrated subdir {dir} → {target}"); } - - if (!Directory.EnumerateFileSystemEntries(legacyConfigDir).Any()) + catch (IOException e) { - Directory.Delete(legacyConfigDir); - Log.Information($"HellionChat: removed empty legacy dir {legacyConfigDir}"); + Log.Warning(e, $"HellionChat: subdir move blocked for {dir}, will retry on next load"); + lockedBlocker = true; } } + + if (!Directory.EnumerateFileSystemEntries(legacyConfigDir).Any()) + { + Directory.Delete(legacyConfigDir); + Log.Information($"HellionChat: removed empty legacy dir {legacyConfigDir}"); + } } catch (Exception e) { Log.Error(e, "HellionChat: layout migration failed, continuing with whatever exists"); } + + if (lockedBlocker) + { + // Surface the most common cause to the user as a notification + // so they don't think Hellion Chat lost their history when in + // fact upstream Chat 2 was still holding the database file. + Notification.AddNotification(new Dalamud.Interface.ImGuiNotification.Notification + { + Title = "Hellion Chat", + Content = "Could not migrate the Chat 2 database — the file appears to be in use. " + + "Disable Chat 2, fully close the game, then start it again. " + + "See the README troubleshooting section if the issue persists.", + Type = Dalamud.Interface.ImGuiNotification.NotificationType.Warning, + InitialDuration = TimeSpan.FromSeconds(30), + }); + } } private void OpenMainUi() diff --git a/README.md b/README.md index 845c0f2..fb2a632 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,9 @@ custom-repo / dev-plugin while the architecture stabilises. ## Install (testers) Hellion Chat is shipped via a Dalamud **custom repository** during the -bootstrap phase. To install: +bootstrap phase. + +### If you have never used Chat 2 1. Open Dalamud settings (`/xlsettings`) → **Experimental**. 2. Add a new entry under **Custom Plugin Repositories**: @@ -51,13 +53,59 @@ bootstrap phase. To install: https://raw.githubusercontent.com/JonKazama-Hellion/HellionChat/main/repo.json ``` 3. Click **Save**, then back in `/xlplugins` hit **All Plugins** and refresh. -4. **Hellion Chat** now appears in the list — install it from there. -5. If you previously had **Chat 2** installed, disable it first. The two - share their database and config dir until Hellion Chat's first launch - migrates everything into `pluginConfigs/HellionChat/`. +4. **Hellion Chat** now appears in the list — install it. + +### If you are migrating from Chat 2 (and want to keep your history) + +The two plugins share `pluginConfigs/ChatTwo/` (database) and +`pluginConfigs/ChatTwo.json` (settings). Hellion Chat moves both into +`pluginConfigs/HellionChat/` on first start, but only if the upstream +plugin isn't holding the database file open. Order matters: + +1. **Disable Chat 2** in `/xlplugins` (don't uninstall, just disable). +2. **Close FFXIV completely** so SQLite releases its file lock — a plain + plugin reload is not enough. +3. Re-launch the game. +4. Add the custom repo URL as in the previous section. +5. Install Hellion Chat. On its first start it migrates the Chat 2 config + file and the entire database directory into the HellionChat layout + without losing data. +6. Verify in **Settings → Privacy → Apply filter to existing database → + Refresh preview** that the message count is what you expect (millions + of rows if you used Chat 2 for a while). + +If the message count comes back as zero, the migration was blocked +(usually because Chat 2 was still active or the previous game session +hadn't fully closed). See the troubleshooting section below. + +### Troubleshooting + +**Hellion Chat shows zero messages but I had Chat 2 history:** +The migration either didn't run or hit a locked file. Close the game, +then move the data manually: + +Linux / XIVLauncher Core: +```bash +mv ~/.xlcore/pluginConfigs/ChatTwo/chat-sqlite.db \ + ~/.xlcore/pluginConfigs/HellionChat/chat-sqlite.db +[ -d ~/.xlcore/pluginConfigs/ChatTwo/EmoteCacheV1 ] && \ + mv ~/.xlcore/pluginConfigs/ChatTwo/EmoteCacheV1 \ + ~/.xlcore/pluginConfigs/HellionChat/ +``` + +Windows / standard XIVLauncher: +```powershell +Move-Item "$env:AppData\XIVLauncher\pluginConfigs\ChatTwo\chat-sqlite.db" ` + "$env:AppData\XIVLauncher\pluginConfigs\HellionChat\chat-sqlite.db" -Force +``` + +Then start the game and Hellion Chat — your full history is back. + +### Updates Updates land in the same plugin list once the maintainer pushes a new -`v0.1.x` tag. +`v0.1.x` tag and re-publishes the GitHub release. No re-installation +needed. ## Why a fork