From 841688a7aa04ad686226d09c9bc54b5b0b1b6266 Mon Sep 17 00:00:00 2001 From: Anna Clemens Date: Sun, 13 Feb 2022 04:36:08 -0500 Subject: [PATCH] feat: add chat database --- ChatTwo/ChatTwo.csproj | 1 + ChatTwo/Chunk.cs | 36 +++-- ChatTwo/Code/ChatCode.cs | 21 ++- ChatTwo/Code/ChatTypeExt.cs | 6 + ChatTwo/Configuration.cs | 6 + ChatTwo/GameFunctions/Chat.cs | 2 +- ChatTwo/Message.cs | 52 ++++++- ChatTwo/PayloadHandler.cs | 24 +-- ChatTwo/Plugin.cs | 11 +- ChatTwo/PluginUi.cs | 24 ++- ChatTwo/Resources/Language.Designer.cs | 63 ++++++++ ChatTwo/Resources/Language.resx | 21 +++ ChatTwo/Store.cs | 183 ++++++++++++++++++----- ChatTwo/Ui/ChatLog.cs | 28 ++-- ChatTwo/Ui/CommandHelp.cs | 2 +- ChatTwo/Ui/Settings.cs | 5 +- ChatTwo/Ui/SettingsTabs/About.cs | 2 +- ChatTwo/Ui/SettingsTabs/ChatColours.cs | 2 +- ChatTwo/Ui/SettingsTabs/Database.cs | 60 ++++++++ ChatTwo/Ui/SettingsTabs/Display.cs | 2 +- ChatTwo/Ui/SettingsTabs/Fonts.cs | 4 +- ChatTwo/Ui/SettingsTabs/ISettingsTab.cs | 2 +- ChatTwo/Ui/SettingsTabs/Miscellaneous.cs | 2 +- ChatTwo/Ui/SettingsTabs/Tabs.cs | 2 +- ChatTwo/Util/ChunkUtil.cs | 10 +- 25 files changed, 476 insertions(+), 95 deletions(-) create mode 100755 ChatTwo/Ui/SettingsTabs/Database.cs diff --git a/ChatTwo/ChatTwo.csproj b/ChatTwo/ChatTwo.csproj index 4ed2c82..ab625c1 100755 --- a/ChatTwo/ChatTwo.csproj +++ b/ChatTwo/ChatTwo.csproj @@ -48,6 +48,7 @@ + diff --git a/ChatTwo/Chunk.cs b/ChatTwo/Chunk.cs index 7c6a53b..a259cf9 100755 --- a/ChatTwo/Chunk.cs +++ b/ChatTwo/Chunk.cs @@ -1,17 +1,33 @@ using ChatTwo.Code; using Dalamud.Game.Text.SeStringHandling; +using LiteDB; namespace ChatTwo; internal abstract class Chunk { + [BsonIgnore] internal Message? Message { get; set; } - internal SeString? Source { get; set; } + + internal ChunkSource Source { get; set; } internal Payload? Link { get; set; } - protected Chunk(SeString? source, Payload? link) { + protected Chunk(ChunkSource source, Payload? link) { this.Source = source; this.Link = link; } + + internal SeString? GetSeString() => this.Source switch { + ChunkSource.None => null, + ChunkSource.Sender => this.Message?.SenderSource, + ChunkSource.Content => this.Message?.ContentSource, + _ => null, + }; +} + +internal enum ChunkSource { + None, + Sender, + Content, } internal class TextChunk : Chunk { @@ -21,23 +37,23 @@ internal class TextChunk : Chunk { internal bool Italic { get; set; } internal string Content { get; set; } - internal TextChunk(SeString? source, Payload? link, string content) : base(source, link) { + internal TextChunk(ChunkSource source, Payload? link, string content) : base(source, link) { this.Content = content; } - internal TextChunk(SeString? source, Payload? link, ChatType? fallbackColour, uint? foreground, uint? glow, bool italic, string content) : base(source, link) { - this.FallbackColour = fallbackColour; - this.Foreground = foreground; - this.Glow = glow; - this.Italic = italic; - this.Content = content; + #pragma warning disable CS8618 + public TextChunk() : base(ChunkSource.None, null) { } + #pragma warning restore CS8618 } internal class IconChunk : Chunk { internal BitmapFontIcon Icon { get; set; } - public IconChunk(SeString? source, Payload? link, BitmapFontIcon icon) : base(source, link) { + public IconChunk(ChunkSource source, Payload? link, BitmapFontIcon icon) : base(source, link) { this.Icon = icon; } + + public IconChunk() : base(ChunkSource.None, null) { + } } diff --git a/ChatTwo/Code/ChatCode.cs b/ChatTwo/Code/ChatCode.cs index 11ddcb3..7b54853 100755 --- a/ChatTwo/Code/ChatCode.cs +++ b/ChatTwo/Code/ChatCode.cs @@ -1,17 +1,30 @@ -namespace ChatTwo.Code; +using LiteDB; + +namespace ChatTwo.Code; internal class ChatCode { private const ushort Clear7 = ~(~0 << 7); internal ushort Raw { get; } - internal ChatType Type => (ChatType) (this.Raw & Clear7); - internal ChatSource Source => this.SourceFrom(11); - internal ChatSource Target => this.SourceFrom(7); + internal ChatType Type { get; } + internal ChatSource Source { get; } + internal ChatSource Target { get; } private ChatSource SourceFrom(ushort shift) => (ChatSource) (1 << ((this.Raw >> shift) & 0xF)); internal ChatCode(ushort raw) { this.Raw = raw; + this.Type = (ChatType) (this.Raw & Clear7); + this.Source = this.SourceFrom(11); + this.Target = this.SourceFrom(7); + } + + [BsonCtor] + public ChatCode(ushort raw, ChatType type, ChatSource source, ChatSource target) { + this.Raw = raw; + this.Type = type; + this.Source = source; + this.Target = target; } internal ChatType Parent() => this.Type switch { diff --git a/ChatTwo/Code/ChatTypeExt.cs b/ChatTwo/Code/ChatTypeExt.cs index 92a1ea9..92e27e5 100755 --- a/ChatTwo/Code/ChatTypeExt.cs +++ b/ChatTwo/Code/ChatTypeExt.cs @@ -345,10 +345,16 @@ internal static class ChatTypeExt { ChatType.LoseDebuff => true, // Announcements + ChatType.System => true, + ChatType.BattleSystem => true, + ChatType.Error => true, + ChatType.LootNotice => true, ChatType.Progress => true, ChatType.LootRoll => true, ChatType.Crafting => true, ChatType.Gathering => true, + ChatType.FreeCompanyLoginLogout => true, + ChatType.PvpTeamLoginLogout => true, _ => false, }; } diff --git a/ChatTwo/Configuration.cs b/ChatTwo/Configuration.cs index a1d8442..827b798 100755 --- a/ChatTwo/Configuration.cs +++ b/ChatTwo/Configuration.cs @@ -27,6 +27,9 @@ internal class Configuration : IPluginConfiguration { public bool CanMove = true; public bool CanResize = true; public bool ShowTitleBar; + public bool DatabaseBattleMessages; + public bool LoadPreviousSession; + public bool FilterIncludePreviousSessions; public float FontSize = 17f; public float JapaneseFontSize = 17f; @@ -54,6 +57,9 @@ internal class Configuration : IPluginConfiguration { this.CanMove = other.CanMove; this.CanResize = other.CanResize; this.ShowTitleBar = other.ShowTitleBar; + this.DatabaseBattleMessages = other.DatabaseBattleMessages; + this.LoadPreviousSession = other.LoadPreviousSession; + this.FilterIncludePreviousSessions = other.FilterIncludePreviousSessions; this.FontSize = other.FontSize; this.JapaneseFontSize = other.JapaneseFontSize; this.SymbolsFontSize = other.SymbolsFontSize; diff --git a/ChatTwo/GameFunctions/Chat.cs b/ChatTwo/GameFunctions/Chat.cs index 756f76f..8159b48 100755 --- a/ChatTwo/GameFunctions/Chat.cs +++ b/ChatTwo/GameFunctions/Chat.cs @@ -503,7 +503,7 @@ internal sealed unsafe class Chat : IDisposable { return ret; } - var nameChunks = ChunkUtil.ToChunks(name, null).ToList(); + var nameChunks = ChunkUtil.ToChunks(name, ChunkSource.None, null).ToList(); if (nameChunks.Count > 0 && nameChunks[0] is TextChunk text) { text.Content = text.Content.TrimStart('\uE01E').TrimStart(); } diff --git a/ChatTwo/Message.cs b/ChatTwo/Message.cs index 679100f..bef5b4c 100755 --- a/ChatTwo/Message.cs +++ b/ChatTwo/Message.cs @@ -1,10 +1,32 @@ using ChatTwo.Code; +using Dalamud.Game.Text.SeStringHandling; +using LiteDB; namespace ChatTwo; +internal class SortCode { + internal ChatType Type { get; set; } + internal ChatSource Source { get; set; } + + internal SortCode(ChatType type, ChatSource source) { + this.Type = type; + this.Source = source; + } + + public SortCode() { + } +} + internal class Message { - internal ulong ContentId; + // ReSharper disable once UnusedMember.Global + internal ObjectId Id { get; } = ObjectId.NewObjectId(); + internal ulong Receiver { get; } + internal ulong ContentId { get; set; } + + [BsonIgnore] internal float? Height; + + [BsonIgnore] internal bool IsVisible; internal DateTime Date { get; } @@ -12,14 +34,40 @@ internal class Message { internal List Sender { get; } internal List Content { get; } - internal Message(ChatCode code, List sender, List content) { + internal SeString SenderSource { get; } + internal SeString ContentSource { get; } + + internal SortCode SortCode { get; } + + internal Message(ulong receiver, ChatCode code, List sender, List content, SeString senderSource, SeString contentSource) { + this.Receiver = receiver; this.Date = DateTime.UtcNow; this.Code = code; this.Sender = sender; this.Content = content; + this.SenderSource = senderSource; + this.ContentSource = contentSource; + this.SortCode = new SortCode(this.Code.Type, this.Code.Source); foreach (var chunk in sender.Concat(content)) { chunk.Message = this; } } + + internal Message(ObjectId id, ulong receiver, ulong contentId, DateTime date, BsonDocument code, BsonArray sender, BsonArray content, BsonDocument senderSource, BsonDocument contentSource, BsonDocument sortCode) { + this.Id = id; + this.Receiver = receiver; + this.ContentId = contentId; + this.Date = date; + this.Code = BsonMapper.Global.ToObject(code); + this.Sender = BsonMapper.Global.Deserialize>(sender); + this.Content = BsonMapper.Global.Deserialize>(content); + this.SenderSource = BsonMapper.Global.ToObject(senderSource); + this.ContentSource = BsonMapper.Global.ToObject(contentSource); + this.SortCode = BsonMapper.Global.ToObject(sortCode); + + foreach (var chunk in this.Sender.Concat(this.Content)) { + chunk.Message = this; + } + } } diff --git a/ChatTwo/PayloadHandler.cs b/ChatTwo/PayloadHandler.cs index 5c88b2a..61b3004 100755 --- a/ChatTwo/PayloadHandler.cs +++ b/ChatTwo/PayloadHandler.cs @@ -195,11 +195,11 @@ internal sealed class PayloadHandler { InlineIcon(icon); } - var name = ChunkUtil.ToChunks(status.Status.Name.ToDalamudString(), null); + var name = ChunkUtil.ToChunks(status.Status.Name.ToDalamudString(), ChunkSource.None, null); this.Log.DrawChunks(name.ToList()); ImGui.Separator(); - var desc = ChunkUtil.ToChunks(status.Status.Description.ToDalamudString(), null); + var desc = ChunkUtil.ToChunks(status.Status.Description.ToDalamudString(), ChunkSource.None, null); this.Log.DrawChunks(desc.ToList()); } @@ -217,11 +217,11 @@ internal sealed class PayloadHandler { InlineIcon(icon); } - var name = ChunkUtil.ToChunks(item.Item.Name.ToDalamudString(), null); + var name = ChunkUtil.ToChunks(item.Item.Name.ToDalamudString(), ChunkSource.None, null); this.Log.DrawChunks(name.ToList()); ImGui.Separator(); - var desc = ChunkUtil.ToChunks(item.Item.Description.ToDalamudString(), null); + var desc = ChunkUtil.ToChunks(item.Item.Description.ToDalamudString(), ChunkSource.None, null); this.Log.DrawChunks(desc.ToList()); } @@ -235,13 +235,13 @@ internal sealed class PayloadHandler { InlineIcon(icon); } - var name = ChunkUtil.ToChunks(item.Name.ToDalamudString(), null); + var name = ChunkUtil.ToChunks(item.Name.ToDalamudString(), ChunkSource.None, null); this.Log.DrawChunks(name.ToList()); ImGui.Separator(); var help = this.Ui.Plugin.DataManager.GetExcelSheet()?.GetRow(payload.RawItemId); if (help != null) { - var desc = ChunkUtil.ToChunks(help.Description.ToDalamudString(), null); + var desc = ChunkUtil.ToChunks(help.Description.ToDalamudString(), ChunkSource.None, null); this.Log.DrawChunks(desc.ToList()); } } @@ -279,7 +279,7 @@ internal sealed class PayloadHandler { } private void ClickLinkPayload(Chunk chunk, Payload payload, DalamudLinkPayload link) { - if (chunk.Source is not { } source) { + if (chunk.GetSeString() is not { } source) { return; } @@ -334,7 +334,7 @@ internal sealed class PayloadHandler { name.Payloads.Add(new TextPayload(" ")); } - this.Log.DrawChunks(ChunkUtil.ToChunks(name, null).ToList(), false); + this.Log.DrawChunks(ChunkUtil.ToChunks(name, ChunkSource.None, null).ToList(), false); ImGui.Separator(); var realItemId = payload.RawItemId; @@ -383,7 +383,7 @@ internal sealed class PayloadHandler { } var name = item.Name.ToDalamudString(); - this.Log.DrawChunks(ChunkUtil.ToChunks(name, null).ToList(), false); + this.Log.DrawChunks(ChunkUtil.ToChunks(name, ChunkSource.None, null).ToList(), false); ImGui.Separator(); var realItemId = payload.RawItemId; @@ -398,11 +398,11 @@ internal sealed class PayloadHandler { } private void DrawPlayerPopup(Chunk chunk, PlayerPayload player) { - var name = new List { new TextChunk(null, null, player.PlayerName) }; + var name = new List { new TextChunk(ChunkSource.None, null, player.PlayerName) }; if (player.World.IsPublic) { name.AddRange(new Chunk[] { - new IconChunk(null, null, BitmapFontIcon.CrossWorld), - new TextChunk(null, null, player.World.Name), + new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld), + new TextChunk(ChunkSource.None, null, player.World.Name), }); } diff --git a/ChatTwo/Plugin.cs b/ChatTwo/Plugin.cs index fe948ec..7666e53 100755 --- a/ChatTwo/Plugin.cs +++ b/ChatTwo/Plugin.cs @@ -1,4 +1,5 @@ -using System.Globalization; +using System.Diagnostics; +using System.Globalization; using ChatTwo.Resources; using Dalamud.Data; using Dalamud.Game; @@ -66,8 +67,12 @@ public sealed class Plugin : IDalamudPlugin { internal int DeferredSaveFrames = -1; + internal DateTime GameStarted { get; } + #pragma warning disable CS8618 public Plugin() { + this.GameStarted = Process.GetCurrentProcess().StartTime.ToUniversalTime(); + this.Config = this.Interface!.GetPluginConfig() as Configuration ?? new Configuration(); this.Config.Migrate(); @@ -79,6 +84,10 @@ public sealed class Plugin : IDalamudPlugin { this.Store = new Store(this); this.Ui = new PluginUi(this); + if (this.Interface.Reason is not PluginLoadReason.Boot) { + this.Store.FilterAllTabs(false); + } + this.Framework!.Update += this.FrameworkUpdate; this.Interface.LanguageChanged += this.LanguageChanged; } diff --git a/ChatTwo/PluginUi.cs b/ChatTwo/PluginUi.cs index c0e9426..8403d42 100755 --- a/ChatTwo/PluginUi.cs +++ b/ChatTwo/PluginUi.cs @@ -132,10 +132,26 @@ internal sealed class PluginUi : IDisposable { component.Dispose(); } - this._regularFont.Item1.Free(); - this._italicFont.Item1.Free(); - this._gameSymFont.Item1.Free(); - this._symRange.Free(); + if (this._regularFont.Item1.IsAllocated) { + this._regularFont.Item1.Free(); + } + + if (this._italicFont.Item1.IsAllocated) { + this._italicFont.Item1.Free(); + } + + if (this._jpFont.Item1.IsAllocated) { + this._jpFont.Item1.Free(); + } + + if (this._gameSymFont.Item1.IsAllocated) { + this._gameSymFont.Item1.Free(); + } + + if (this._symRange.IsAllocated) { + this._symRange.Free(); + } + this._fontCfg.Destroy(); this._fontCfgMerge.Destroy(); } diff --git a/ChatTwo/Resources/Language.Designer.cs b/ChatTwo/Resources/Language.Designer.cs index 313d611..8a083ad 100755 --- a/ChatTwo/Resources/Language.Designer.cs +++ b/ChatTwo/Resources/Language.Designer.cs @@ -402,6 +402,33 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Database. + /// + internal static string Options_Database_Tab { + get { + return ResourceManager.GetString("Options_Database_Tab", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If battle messages are saved to the database, the size of the database will grow much faster, and there will be a noticeable freeze when saving settings. It is recommended to leave this disabled.. + /// + internal static string Options_DatabaseBattleMessages_Description { + get { + return ResourceManager.GetString("Options_DatabaseBattleMessages_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Save battle messages in database. + /// + internal static string Options_DatabaseBattleMessages_Name { + get { + return ResourceManager.GetString("Options_DatabaseBattleMessages_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Display. /// @@ -411,6 +438,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to Include messages from before the game was launched when populating tabs. Tabs are populated when the settings are saved.. + /// + internal static string Options_FilterIncludePreviousSessions_Description { + get { + return ResourceManager.GetString("Options_FilterIncludePreviousSessions_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Include previous sessions when populating tabs. + /// + internal static string Options_FilterIncludePreviousSessions_Name { + get { + return ResourceManager.GetString("Options_FilterIncludePreviousSessions_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to The font {0} will use to display non-Japanese text.. /// @@ -591,6 +636,24 @@ namespace ChatTwo.Resources { } } + /// + /// Looks up a localized string similar to After logging in, populate the chat with the messages from when you last logged off.. + /// + internal static string Options_LoadPreviousSession_Description { + get { + return ResourceManager.GetString("Options_LoadPreviousSession_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Display previous chat session on login. + /// + internal static string Options_LoadPreviousSession_Name { + get { + return ResourceManager.GetString("Options_LoadPreviousSession_Name", resourceCulture); + } + } + /// /// Looks up a localized string similar to Miscellaneous. /// diff --git a/ChatTwo/Resources/Language.resx b/ChatTwo/Resources/Language.resx index 2a6923c..aedb6f3 100755 --- a/ChatTwo/Resources/Language.resx +++ b/ChatTwo/Resources/Language.resx @@ -437,4 +437,25 @@ The language to display {0} in. + + Save battle messages in database + + + If battle messages are saved to the database, the size of the database will grow much faster, and there will be a noticeable freeze when saving settings. It is recommended to leave this disabled. + + + Display previous chat session on login + + + After logging in, populate the chat with the messages from when you last logged off. + + + Database + + + Include previous sessions when populating tabs + + + Include messages from before the game was launched when populating tabs. Tabs are populated when the settings are saved. + diff --git a/ChatTwo/Store.cs b/ChatTwo/Store.cs index b924ab4..03cf736 100755 --- a/ChatTwo/Store.cs +++ b/ChatTwo/Store.cs @@ -1,9 +1,11 @@ using System.Collections.Concurrent; +using System.Diagnostics; using ChatTwo.Code; using ChatTwo.Util; using Dalamud.Game; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; +using LiteDB; using Lumina.Excel.GeneratedSheets; namespace ChatTwo; @@ -11,63 +13,146 @@ namespace ChatTwo; internal class Store : IDisposable { internal const int MessagesLimit = 10_000; - internal sealed class MessagesLock : IDisposable { - private Mutex Mutex { get; } - internal List Messages { get; } - - internal MessagesLock(List messages, Mutex mutex) { - this.Messages = messages; - this.Mutex = mutex; - - this.Mutex.WaitOne(); - } - - public void Dispose() { - this.Mutex.ReleaseMutex(); - } - } - private Plugin Plugin { get; } - private Mutex MessagesMutex { get; } = new(); - private List Messages { get; } = new(); private ConcurrentQueue<(uint, Message)> Pending { get; } = new(); + private Stopwatch CheckpointTimer { get; } = new(); + internal ILiteDatabase Database { get; } + private ILiteCollection Messages => this.Database.GetCollection("messages"); private Dictionary Formats { get; } = new(); + private ulong LastContentId { get; set; } + + private ulong CurrentContentId { + get { + var contentId = this.Plugin.ClientState.LocalContentId; + return contentId == 0 ? this.LastContentId : contentId; + } + } internal Store(Plugin plugin) { this.Plugin = plugin; + this.CheckpointTimer.Start(); + + var dir = this.Plugin.Interface.ConfigDirectory; + dir.Create(); + + BsonMapper.Global = new BsonMapper { + IncludeNonPublic = true, + TrimWhitespace = false, + // EnumAsInteger = true, + }; + BsonMapper.Global.Entity() + .Id(msg => msg.Id) + .Ctor(doc => new Message( + doc["_id"].AsObjectId, + (ulong) doc["Receiver"].AsInt64, + (ulong) doc["ContentId"].AsInt64, + DateTime.UnixEpoch.AddMilliseconds(doc["Date"].AsInt64), + doc["Code"].AsDocument, + doc["Sender"].AsArray, + doc["Content"].AsArray, + doc["SenderSource"].AsDocument, + doc["ContentSource"].AsDocument, + doc["SortCode"].AsDocument + )); + BsonMapper.Global.RegisterType( + payload => { + switch (payload) { + case AchievementPayload achievement: + return new BsonDocument(new Dictionary { + ["Type"] = new("Achievement"), + ["Id"] = new(achievement.Id), + }); + case PartyFinderPayload partyFinder: + return new BsonDocument(new Dictionary { + ["Type"] = new("PartyFinder"), + ["Id"] = new(partyFinder.Id), + }); + } + + return payload?.Encode(); + }, + bson => { + if (bson.IsNull) { + return null; + } + + if (bson.IsDocument) { + return bson["Type"].AsString switch { + "Achievement" => new AchievementPayload((uint) bson["Id"].AsInt64), + "PartyFinder" => new PartyFinderPayload((uint) bson["Id"].AsInt64), + _ => null, + }; + } + + return Payload.Decode(new BinaryReader(new MemoryStream(bson.AsBinary))); + }); + BsonMapper.Global.RegisterType( + type => (int) type, + bson => (ChatType) bson.AsInt32 + ); + BsonMapper.Global.RegisterType( + source => (int) source, + bson => (ChatSource) bson.AsInt32 + ); + BsonMapper.Global.RegisterType( + dateTime => dateTime.Subtract(DateTime.UnixEpoch).TotalMilliseconds, + bson => DateTime.UnixEpoch.AddMilliseconds(bson.AsInt64) + ); + this.Database = new LiteDatabase(Path.Join(dir.FullName, "chat.db"), BsonMapper.Global) { + CheckpointSize = 1_000, + Timeout = TimeSpan.FromSeconds(1), + }; + this.Messages.EnsureIndex(msg => msg.Date); + this.Messages.EnsureIndex(msg => msg.SortCode); this.Plugin.ChatGui.ChatMessageUnhandled += this.ChatMessage; this.Plugin.Framework.Update += this.GetMessageInfo; + this.Plugin.Framework.Update += this.UpdateReceiver; + this.Plugin.ClientState.Logout += this.Logout; } public void Dispose() { + this.Plugin.ClientState.Logout -= this.Logout; + this.Plugin.Framework.Update -= this.UpdateReceiver; this.Plugin.Framework.Update -= this.GetMessageInfo; this.Plugin.ChatGui.ChatMessageUnhandled -= this.ChatMessage; - this.MessagesMutex.Dispose(); + this.Database.Dispose(); + } + + private void Logout(object? sender, EventArgs eventArgs) { + this.LastContentId = 0; + } + + private void UpdateReceiver(Framework framework) { + var contentId = this.Plugin.ClientState.LocalContentId; + if (contentId != 0) { + this.LastContentId = contentId; + } } private void GetMessageInfo(Framework framework) { + if (this.CheckpointTimer.Elapsed > TimeSpan.FromMinutes(5)) { + this.CheckpointTimer.Restart(); + new Thread(() => this.Database.Checkpoint()).Start(); + } + if (!this.Pending.TryDequeue(out var entry)) { return; } var contentId = this.Plugin.Functions.Chat.GetContentIdForEntry(entry.Item1); entry.Item2.ContentId = contentId ?? 0; - } - - internal MessagesLock GetMessages() { - return new MessagesLock(this.Messages, this.MessagesMutex); + if (this.Plugin.Config.DatabaseBattleMessages || !entry.Item2.Code.IsBattle()) { + this.Messages.Update(entry.Item2); + } } internal void AddMessage(Message message, Tab? currentTab) { - using var messages = this.GetMessages(); - messages.Messages.Add(message); - - while (messages.Messages.Count > MessagesLimit) { - messages.Messages.RemoveAt(0); + if (this.Plugin.Config.DatabaseBattleMessages || !message.Code.IsBattle()) { + this.Messages.Insert(message); } var currentMatches = currentTab?.Matches(message) ?? false; @@ -88,12 +173,36 @@ internal class Store : IDisposable { } internal void FilterTab(Tab tab, bool unread) { - using var messages = this.GetMessages(); - foreach (var message in messages.Messages) { - if (tab.Matches(message)) { - tab.AddMessage(message, unread); + var sortCodes = new List(); + foreach (var (type, sources) in tab.ChatCodes) { + sortCodes.Add(new SortCode(type, 0)); + sortCodes.Add(new SortCode(type, (ChatSource) 1)); + + if (type.HasSource()) { + foreach (var source in Enum.GetValues()) { + if (sources.HasFlag(source)) { + sortCodes.Add(new SortCode(type, source)); + } + } } } + + var query = this.Messages + .Query() + .OrderByDescending(msg => msg.Date) + .Where(msg => sortCodes.Contains(msg.SortCode)) + .Where(msg => msg.Receiver == this.CurrentContentId); + if (!this.Plugin.Config.FilterIncludePreviousSessions) { + query = query.Where(msg => msg.Date >= this.Plugin.GameStarted); + } + + var messages = query + .Limit(MessagesLimit) + .ToEnumerable() + .Reverse(); + foreach (var message in messages) { + tab.AddMessage(message, unread); + } } private void ChatMessage(XivChatType type, uint senderId, SeString sender, SeString message) { @@ -106,18 +215,18 @@ internal class Store : IDisposable { var senderChunks = new List(); if (formatting is { IsPresent: true }) { - senderChunks.Add(new TextChunk(null, null, formatting.Before) { + senderChunks.Add(new TextChunk(ChunkSource.None, null, formatting.Before) { FallbackColour = chatCode.Type, }); - senderChunks.AddRange(ChunkUtil.ToChunks(sender, chatCode.Type)); - senderChunks.Add(new TextChunk(null, null, formatting.After) { + senderChunks.AddRange(ChunkUtil.ToChunks(sender, ChunkSource.Sender, chatCode.Type)); + senderChunks.Add(new TextChunk(ChunkSource.None, null, formatting.After) { FallbackColour = chatCode.Type, }); } - var messageChunks = ChunkUtil.ToChunks(message, chatCode.Type).ToList(); + var messageChunks = ChunkUtil.ToChunks(message, ChunkSource.Content, chatCode.Type).ToList(); - var msg = new Message(chatCode, senderChunks, messageChunks); + var msg = new Message(this.CurrentContentId, chatCode, senderChunks, messageChunks, sender, message); this.AddMessage(msg, this.Plugin.Ui.CurrentTab); var idx = this.Plugin.Functions.GetCurrentChatLogEntryIndex(); diff --git a/ChatTwo/Ui/ChatLog.cs b/ChatTwo/Ui/ChatLog.cs index 3df9924..90a6f30 100755 --- a/ChatTwo/Ui/ChatLog.cs +++ b/ChatTwo/Ui/ChatLog.cs @@ -53,14 +53,28 @@ internal sealed class ChatLog : IUiComponent { this._fontIcon = this.Ui.Plugin.DataManager.GetImGuiTexture("common/font/fonticon_ps5.tex"); this.Ui.Plugin.Functions.Chat.Activated += this.Activated; + this.Ui.Plugin.ClientState.Login += this.Login; + this.Ui.Plugin.ClientState.Logout += this.Logout; } public void Dispose() { + this.Ui.Plugin.ClientState.Logout -= this.Logout; + this.Ui.Plugin.ClientState.Login -= this.Login; this.Ui.Plugin.Functions.Chat.Activated -= this.Activated; this._fontIcon?.Dispose(); this.Ui.Plugin.CommandManager.RemoveHandler("/clearlog2"); } + private void Logout(object? sender, EventArgs e) { + foreach (var tab in this.Ui.Plugin.Config.Tabs) { + tab.Clear(); + } + } + + private void Login(object? sender, EventArgs e) { + this.Ui.Plugin.Store.FilterAllTabs(false); + } + private void Activated(ChatActivatedArgs args) { this.Activate = true; if (args.AddIfNotPresent != null && !this.Chat.Contains(args.AddIfNotPresent)) { @@ -126,10 +140,6 @@ internal sealed class ChatLog : IUiComponent { private void ClearLog(string command, string arguments) { switch (arguments) { case "all": - using (var messages = this.Ui.Plugin.Store.GetMessages()) { - messages.Messages.Clear(); - } - foreach (var tab in this.Ui.Plugin.Config.Tabs) { tab.Clear(); } @@ -374,10 +384,10 @@ internal sealed class ChatLog : IUiComponent { ?.RawString ?? "???"; this.DrawChunks(new Chunk[] { - new TextChunk(null, null, "Tell "), - new TextChunk(null, null, this._tellTarget.Name), - new IconChunk(null, null, BitmapFontIcon.CrossWorld), - new TextChunk(null, null, world), + new TextChunk(ChunkSource.None, null, "Tell "), + new TextChunk(ChunkSource.None, null, this._tellTarget.Name), + new IconChunk(ChunkSource.None, null, BitmapFontIcon.CrossWorld), + new TextChunk(ChunkSource.None, null, world), }); } else if (this._tempChannel != null) { if (this._tempChannel.Value.IsLinkshell()) { @@ -626,7 +636,7 @@ internal sealed class ChatLog : IUiComponent { if (table) { ImGui.TextUnformatted(timestamp); } else { - this.DrawChunk(new TextChunk(null, null, $"[{timestamp}]") { + this.DrawChunk(new TextChunk(ChunkSource.None, null, $"[{timestamp}]") { Foreground = 0xFFFFFFFF, }); ImGui.SameLine(); diff --git a/ChatTwo/Ui/CommandHelp.cs b/ChatTwo/Ui/CommandHelp.cs index b3c3e62..b188a9d 100755 --- a/ChatTwo/Ui/CommandHelp.cs +++ b/ChatTwo/Ui/CommandHelp.cs @@ -50,7 +50,7 @@ internal class CommandHelp { return; } - this.Log.DrawChunks(ChunkUtil.ToChunks(this.Command.Description.ToDalamudString(), null).ToList()); + this.Log.DrawChunks(ChunkUtil.ToChunks(this.Command.Description.ToDalamudString(), ChunkSource.None, null).ToList()); ImGui.End(); } diff --git a/ChatTwo/Ui/Settings.cs b/ChatTwo/Ui/Settings.cs index d601526..b2dccec 100755 --- a/ChatTwo/Ui/Settings.cs +++ b/ChatTwo/Ui/Settings.cs @@ -26,6 +26,7 @@ internal sealed class Settings : IUiComponent { new Ui.SettingsTabs.Fonts(this.Mutable), new ChatColours(this.Mutable, this.Ui.Plugin), new Tabs(this.Mutable), + new Database(this.Mutable, this.Ui.Plugin.Store), new Miscellaneous(this.Mutable), new About(), }; @@ -70,9 +71,11 @@ internal sealed class Settings : IUiComponent { ImGui.TableNextColumn(); + var changed = false; for (var i = 0; i < this.Tabs.Count; i++) { if (ImGui.Selectable($"{this.Tabs[i].Name}###tab-{i}", this._currentTab == i)) { this._currentTab = i; + changed = true; } } @@ -84,7 +87,7 @@ internal sealed class Settings : IUiComponent { - ImGui.GetStyle().ItemInnerSpacing.Y * 2 - ImGui.CalcTextSize("A").Y; if (ImGui.BeginChild("##chat2-settings", new Vector2(-1, height))) { - this.Tabs[this._currentTab].Draw(); + this.Tabs[this._currentTab].Draw(changed); ImGui.EndChild(); } diff --git a/ChatTwo/Ui/SettingsTabs/About.cs b/ChatTwo/Ui/SettingsTabs/About.cs index 1f89acc..93bfcf5 100755 --- a/ChatTwo/Ui/SettingsTabs/About.cs +++ b/ChatTwo/Ui/SettingsTabs/About.cs @@ -29,7 +29,7 @@ internal sealed class About : ISettingsTab { this._translators.Sort((a, b) => string.Compare(a.ToLowerInvariant(), b.ToLowerInvariant(), StringComparison.Ordinal)); } - public void Draw() { + public void Draw(bool changed) { ImGui.PushTextWrapPos(); ImGui.TextUnformatted(string.Format(Language.Options_About_Opening, Plugin.PluginName)); diff --git a/ChatTwo/Ui/SettingsTabs/ChatColours.cs b/ChatTwo/Ui/SettingsTabs/ChatColours.cs index dfeb028..13105da 100755 --- a/ChatTwo/Ui/SettingsTabs/ChatColours.cs +++ b/ChatTwo/Ui/SettingsTabs/ChatColours.cs @@ -32,7 +32,7 @@ internal sealed class ChatColours : ISettingsTab { #endif } - public void Draw() { + public void Draw(bool changed) { foreach (var (_, types) in ChatTypeExt.SortOrder) { foreach (var type in types) { if (ImGuiUtil.IconButton(FontAwesomeIcon.UndoAlt, $"{type}", Language.Options_ChatColours_Reset)) { diff --git a/ChatTwo/Ui/SettingsTabs/Database.cs b/ChatTwo/Ui/SettingsTabs/Database.cs new file mode 100755 index 0000000..2a74ce2 --- /dev/null +++ b/ChatTwo/Ui/SettingsTabs/Database.cs @@ -0,0 +1,60 @@ +using ChatTwo.Resources; +using ChatTwo.Util; +using ImGuiNET; + +namespace ChatTwo.Ui.SettingsTabs; + +internal sealed class Database : ISettingsTab { + private Configuration Mutable { get; } + private Store Store { get; } + + public string Name => Language.Options_Database_Tab + "###tabs-database"; + + internal Database(Configuration mutable, Store store) { + this.Store = store; + this.Mutable = mutable; + } + + private bool _showAdvanced; + + public void Draw(bool changed) { + if (changed) { + this._showAdvanced = ImGui.GetIO().KeyShift; + } + + ImGuiUtil.OptionCheckbox(ref this.Mutable.DatabaseBattleMessages, Language.Options_DatabaseBattleMessages_Name, Language.Options_DatabaseBattleMessages_Description); + ImGui.Spacing(); + + if (ImGuiUtil.OptionCheckbox(ref this.Mutable.LoadPreviousSession, Language.Options_LoadPreviousSession_Name, Language.Options_LoadPreviousSession_Description)) { + if (this.Mutable.LoadPreviousSession) { + this.Mutable.FilterIncludePreviousSessions = true; + } + } + + ImGui.Spacing(); + + if (ImGuiUtil.OptionCheckbox(ref this.Mutable.FilterIncludePreviousSessions, Language.Options_FilterIncludePreviousSessions_Name, Language.Options_FilterIncludePreviousSessions_Description)) { + if (!this.Mutable.FilterIncludePreviousSessions) { + this.Mutable.LoadPreviousSession = false; + } + } + + ImGui.Spacing(); + + if (this._showAdvanced && ImGui.TreeNodeEx("Advanced")) { + ImGui.PushTextWrapPos(); + ImGuiUtil.WarningText("Do not click these buttons unless you know what you're doing."); + + if (ImGui.Button("Checkpoint")) { + this.Store.Database.Checkpoint(); + } + + if (ImGui.Button("Rebuild")) { + this.Store.Database.Rebuild(); + } + + ImGui.PopTextWrapPos(); + ImGui.TreePop(); + } + } +} diff --git a/ChatTwo/Ui/SettingsTabs/Display.cs b/ChatTwo/Ui/SettingsTabs/Display.cs index 716726a..830691d 100755 --- a/ChatTwo/Ui/SettingsTabs/Display.cs +++ b/ChatTwo/Ui/SettingsTabs/Display.cs @@ -13,7 +13,7 @@ internal sealed class Display : ISettingsTab { this.Mutable = mutable; } - public void Draw() { + public void Draw(bool changed) { ImGui.PushTextWrapPos(); ImGuiUtil.OptionCheckbox(ref this.Mutable.HideChat, Language.Options_HideChat_Name, Language.Options_HideChat_Description); diff --git a/ChatTwo/Ui/SettingsTabs/Fonts.cs b/ChatTwo/Ui/SettingsTabs/Fonts.cs index 7287a59..fd1d2ee 100755 --- a/ChatTwo/Ui/SettingsTabs/Fonts.cs +++ b/ChatTwo/Ui/SettingsTabs/Fonts.cs @@ -21,8 +21,8 @@ public class Fonts : ISettingsTab { this.JpFonts = Ui.Fonts.GetJpFonts(); } - public void Draw() { - if (ImGui.IsWindowAppearing()) { + public void Draw(bool changed) { + if (changed) { this.UpdateFonts(); } diff --git a/ChatTwo/Ui/SettingsTabs/ISettingsTab.cs b/ChatTwo/Ui/SettingsTabs/ISettingsTab.cs index f62573d..aecbe69 100755 --- a/ChatTwo/Ui/SettingsTabs/ISettingsTab.cs +++ b/ChatTwo/Ui/SettingsTabs/ISettingsTab.cs @@ -2,5 +2,5 @@ internal interface ISettingsTab { string Name { get; } - void Draw(); + void Draw(bool changed); } diff --git a/ChatTwo/Ui/SettingsTabs/Miscellaneous.cs b/ChatTwo/Ui/SettingsTabs/Miscellaneous.cs index 250332f..fd0f6e6 100755 --- a/ChatTwo/Ui/SettingsTabs/Miscellaneous.cs +++ b/ChatTwo/Ui/SettingsTabs/Miscellaneous.cs @@ -13,7 +13,7 @@ internal sealed class Miscellaneous : ISettingsTab { this.Mutable = mutable; } - public void Draw() { + public void Draw(bool changed) { if (ImGuiUtil.BeginComboVertical(Language.Options_Language_Name, this.Mutable.LanguageOverride.Name())) { foreach (var language in Enum.GetValues()) { if (ImGui.Selectable(language.Name())) { diff --git a/ChatTwo/Ui/SettingsTabs/Tabs.cs b/ChatTwo/Ui/SettingsTabs/Tabs.cs index ce31cad..53fc9e1 100755 --- a/ChatTwo/Ui/SettingsTabs/Tabs.cs +++ b/ChatTwo/Ui/SettingsTabs/Tabs.cs @@ -17,7 +17,7 @@ internal sealed class Tabs : ISettingsTab { this.Mutable = mutable; } - public void Draw() { + public void Draw(bool changed) { const string addTabPopup = "add-tab-popup"; if (ImGuiUtil.IconButton(FontAwesomeIcon.Plus, tooltip: Language.Options_Tabs_Add)) { diff --git a/ChatTwo/Util/ChunkUtil.cs b/ChatTwo/Util/ChunkUtil.cs index 4674da1..7d85de7 100755 --- a/ChatTwo/Util/ChunkUtil.cs +++ b/ChatTwo/Util/ChunkUtil.cs @@ -5,7 +5,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads; namespace ChatTwo.Util; internal static class ChunkUtil { - internal static IEnumerable ToChunks(SeString msg, ChatType? defaultColour) { + internal static IEnumerable ToChunks(SeString msg, ChunkSource source, ChatType? defaultColour) { var chunks = new List(); var italic = false; @@ -14,7 +14,7 @@ internal static class ChunkUtil { Payload? link = null; void Append(string text) { - chunks.Add(new TextChunk(msg, link, text) { + chunks.Add(new TextChunk(source, link, text) { FallbackColour = defaultColour, Foreground = foreground.Count > 0 ? foreground.Peek() : null, Glow = glow.Count > 0 ? glow.Peek() : null, @@ -47,13 +47,13 @@ internal static class ChunkUtil { break; case PayloadType.AutoTranslateText: - chunks.Add(new IconChunk(msg, link, BitmapFontIcon.AutoTranslateBegin)); + chunks.Add(new IconChunk(source, link, BitmapFontIcon.AutoTranslateBegin)); var autoText = ((AutoTranslatePayload) payload).Text; Append(autoText.Substring(2, autoText.Length - 4)); - chunks.Add(new IconChunk(msg, link, BitmapFontIcon.AutoTranslateEnd)); + chunks.Add(new IconChunk(source, link, BitmapFontIcon.AutoTranslateEnd)); break; case PayloadType.Icon: - chunks.Add(new IconChunk(msg, link, ((IconPayload) payload).Icon)); + chunks.Add(new IconChunk(source, link, ((IconPayload) payload).Icon)); break; case PayloadType.MapLink: case PayloadType.Quest: