feat: add chat database
This commit is contained in:
parent
18c311ace5
commit
9b7b84a58d
|
@ -48,6 +48,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DalamudPackager" Version="2.1.5"/>
|
||||
<PackageReference Include="LiteDB" Version="5.0.11"/>
|
||||
<PackageReference Include="SharpDX.Direct2D1" Version="4.2.0"/>
|
||||
<PackageReference Include="XivCommon" Version="5.0.0"/>
|
||||
</ItemGroup>
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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<Chunk> Sender { get; }
|
||||
internal List<Chunk> Content { get; }
|
||||
|
||||
internal Message(ChatCode code, List<Chunk> sender, List<Chunk> content) {
|
||||
internal SeString SenderSource { get; }
|
||||
internal SeString ContentSource { get; }
|
||||
|
||||
internal SortCode SortCode { get; }
|
||||
|
||||
internal Message(ulong receiver, ChatCode code, List<Chunk> sender, List<Chunk> 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<ChatCode>(code);
|
||||
this.Sender = BsonMapper.Global.Deserialize<List<Chunk>>(sender);
|
||||
this.Content = BsonMapper.Global.Deserialize<List<Chunk>>(content);
|
||||
this.SenderSource = BsonMapper.Global.ToObject<SeString>(senderSource);
|
||||
this.ContentSource = BsonMapper.Global.ToObject<SeString>(contentSource);
|
||||
this.SortCode = BsonMapper.Global.ToObject<SortCode>(sortCode);
|
||||
|
||||
foreach (var chunk in this.Sender.Concat(this.Content)) {
|
||||
chunk.Message = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<EventItemHelp>()?.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<Chunk> { new TextChunk(null, null, player.PlayerName) };
|
||||
var name = new List<Chunk> { 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),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -402,6 +402,33 @@ namespace ChatTwo.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Database.
|
||||
/// </summary>
|
||||
internal static string Options_Database_Tab {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_Database_Tab", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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..
|
||||
/// </summary>
|
||||
internal static string Options_DatabaseBattleMessages_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_DatabaseBattleMessages_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Save battle messages in database.
|
||||
/// </summary>
|
||||
internal static string Options_DatabaseBattleMessages_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_DatabaseBattleMessages_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Display.
|
||||
/// </summary>
|
||||
|
@ -411,6 +438,24 @@ namespace ChatTwo.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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..
|
||||
/// </summary>
|
||||
internal static string Options_FilterIncludePreviousSessions_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_FilterIncludePreviousSessions_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Include previous sessions when populating tabs.
|
||||
/// </summary>
|
||||
internal static string Options_FilterIncludePreviousSessions_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_FilterIncludePreviousSessions_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The font {0} will use to display non-Japanese text..
|
||||
/// </summary>
|
||||
|
@ -591,6 +636,24 @@ namespace ChatTwo.Resources {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to After logging in, populate the chat with the messages from when you last logged off..
|
||||
/// </summary>
|
||||
internal static string Options_LoadPreviousSession_Description {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_LoadPreviousSession_Description", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Display previous chat session on login.
|
||||
/// </summary>
|
||||
internal static string Options_LoadPreviousSession_Name {
|
||||
get {
|
||||
return ResourceManager.GetString("Options_LoadPreviousSession_Name", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Miscellaneous.
|
||||
/// </summary>
|
||||
|
|
|
@ -437,4 +437,25 @@
|
|||
<data name="Options_Language_Description" xml:space="preserve">
|
||||
<value>The language to display {0} in.</value>
|
||||
</data>
|
||||
<data name="Options_DatabaseBattleMessages_Name" xml:space="preserve">
|
||||
<value>Save battle messages in database</value>
|
||||
</data>
|
||||
<data name="Options_DatabaseBattleMessages_Description" xml:space="preserve">
|
||||
<value>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.</value>
|
||||
</data>
|
||||
<data name="Options_LoadPreviousSession_Name" xml:space="preserve">
|
||||
<value>Display previous chat session on login</value>
|
||||
</data>
|
||||
<data name="Options_LoadPreviousSession_Description" xml:space="preserve">
|
||||
<value>After logging in, populate the chat with the messages from when you last logged off.</value>
|
||||
</data>
|
||||
<data name="Options_Database_Tab" xml:space="preserve">
|
||||
<value>Database</value>
|
||||
</data>
|
||||
<data name="Options_FilterIncludePreviousSessions_Name" xml:space="preserve">
|
||||
<value>Include previous sessions when populating tabs</value>
|
||||
</data>
|
||||
<data name="Options_FilterIncludePreviousSessions_Description" xml:space="preserve">
|
||||
<value>Include messages from before the game was launched when populating tabs. Tabs are populated when the settings are saved.</value>
|
||||
</data>
|
||||
</root>
|
||||
|
|
183
ChatTwo/Store.cs
183
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<Message> Messages { get; }
|
||||
|
||||
internal MessagesLock(List<Message> 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<Message> Messages { get; } = new();
|
||||
private ConcurrentQueue<(uint, Message)> Pending { get; } = new();
|
||||
private Stopwatch CheckpointTimer { get; } = new();
|
||||
internal ILiteDatabase Database { get; }
|
||||
private ILiteCollection<Message> Messages => this.Database.GetCollection<Message>("messages");
|
||||
|
||||
private Dictionary<ChatType, NameFormatting> 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<Message>()
|
||||
.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?>(
|
||||
payload => {
|
||||
switch (payload) {
|
||||
case AchievementPayload achievement:
|
||||
return new BsonDocument(new Dictionary<string, BsonValue> {
|
||||
["Type"] = new("Achievement"),
|
||||
["Id"] = new(achievement.Id),
|
||||
});
|
||||
case PartyFinderPayload partyFinder:
|
||||
return new BsonDocument(new Dictionary<string, BsonValue> {
|
||||
["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<SortCode>();
|
||||
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<ChatSource>()) {
|
||||
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<Chunk>();
|
||||
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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
|
||||
internal interface ISettingsTab {
|
||||
string Name { get; }
|
||||
void Draw();
|
||||
void Draw(bool changed);
|
||||
}
|
||||
|
|
|
@ -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<LanguageOverride>()) {
|
||||
if (ImGui.Selectable(language.Name())) {
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -5,7 +5,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|||
namespace ChatTwo.Util;
|
||||
|
||||
internal static class ChunkUtil {
|
||||
internal static IEnumerable<Chunk> ToChunks(SeString msg, ChatType? defaultColour) {
|
||||
internal static IEnumerable<Chunk> ToChunks(SeString msg, ChunkSource source, ChatType? defaultColour) {
|
||||
var chunks = new List<Chunk>();
|
||||
|
||||
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:
|
||||
|
|
Loading…
Reference in New Issue