From 7b1cbbd8f7e7dcd3e17369a26b5fd97326a7f5b9 Mon Sep 17 00:00:00 2001 From: Anna Date: Mon, 17 Jun 2024 13:25:46 -0400 Subject: [PATCH] feat: add conditional replacements --- Model/IWhen.cs | 40 ++++++++++++++ Model/Replacement.cs | 9 ++++ Model/WhenQuest.cs | 22 ++++++++ Model/WhenStatus.cs | 6 +++ Plugin.cs | 124 ++++++++++++++++++++++++++----------------- TimePasses.csproj | 4 ++ Util/OnDispose.cs | 19 +++++++ replacements.yaml | 11 ++-- 8 files changed, 184 insertions(+), 51 deletions(-) create mode 100644 Model/IWhen.cs create mode 100644 Model/Replacement.cs create mode 100644 Model/WhenQuest.cs create mode 100644 Model/WhenStatus.cs create mode 100644 Util/OnDispose.cs diff --git a/Model/IWhen.cs b/Model/IWhen.cs new file mode 100644 index 0000000..d6239a7 --- /dev/null +++ b/Model/IWhen.cs @@ -0,0 +1,40 @@ +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace TimePasses.Model; + +public interface IWhen { + string Text { get; } + bool Slowly { get; } + + bool IsValid(Plugin plugin); +} + +public class WhenConverter : IYamlTypeConverter { + public bool Accepts(Type type) { + // FIXME? typeof(IWhen).IsAssignableFrom(type) + return typeof(IWhen) == type; + } + + public object? ReadYaml(IParser parser, Type type) { + parser.Consume(); + var name = parser.Consume(); + if (!name.IsKey) { + throw new YamlException("invalid when: missing key"); + } + + switch (name.Value) { + case "quest": { + return Plugin.Deserializer.Deserialize(parser); + } + default: { + throw new YamlException($"invalid when: unknown type {name.Value}"); + } + } + } + + public void WriteYaml(IEmitter emitter, object? value, Type type) { + throw new NotImplementedException(); + } +} diff --git a/Model/Replacement.cs b/Model/Replacement.cs new file mode 100644 index 0000000..3e8c615 --- /dev/null +++ b/Model/Replacement.cs @@ -0,0 +1,9 @@ +namespace TimePasses.Model; + +[Serializable] +internal class Replacement { + public uint Id { get; init; } + public string? Text { get; init; } + public bool Slowly { get; init; } + public IWhen[] When { get; init; } = []; +} diff --git a/Model/WhenQuest.cs b/Model/WhenQuest.cs new file mode 100644 index 0000000..ad26b66 --- /dev/null +++ b/Model/WhenQuest.cs @@ -0,0 +1,22 @@ +using FFXIVClientStructs.FFXIV.Client.Game; + +namespace TimePasses.Model; + +[Serializable] +public class WhenQuest : IWhen { + public uint Id { get; init; } + public QuestStatus Status { get; init; } + public string Text { get; init; } + public bool Slowly { get; init; } + + public unsafe bool IsValid(Plugin plugin) { + var complete = QuestManager.IsQuestComplete(this.Id); + var accepted = QuestManager.Instance()->IsQuestAccepted(this.Id); + return this.Status switch { + QuestStatus.Complete when complete => true, + QuestStatus.Incomplete when !complete => true, + QuestStatus.InProgress when accepted && !complete => true, + _ => false, + }; + } +} diff --git a/Model/WhenStatus.cs b/Model/WhenStatus.cs new file mode 100644 index 0000000..4fe62ab --- /dev/null +++ b/Model/WhenStatus.cs @@ -0,0 +1,6 @@ +[Serializable] +public enum QuestStatus { + Complete, + Incomplete, + InProgress, +} diff --git a/Plugin.cs b/Plugin.cs index 15880fa..a76ea50 100644 --- a/Plugin.cs +++ b/Plugin.cs @@ -6,6 +6,7 @@ using Dalamud.IoC; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; +using TimePasses.Model; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; @@ -21,9 +22,14 @@ public class Plugin : IDalamudPlugin { private HttpClient Client { get; } = new(); private DataFile Data { get; set; } - private Dictionary ReplacementPointers { get; } = []; + private Dictionary<(string, bool), nint> ReplacementPointers { get; } = []; private SemaphoreSlim Mutex { get; } = new(1, 1); + internal static IDeserializer Deserializer { get; } = new DeserializerBuilder() + .WithNamingConvention(UnderscoredNamingConvention.Instance) + .WithTypeConverter(new WhenConverter()) + .Build(); + private static class Signatures { internal const string GetBalloonRow = "E8 ?? ?? ?? ?? 48 85 C0 74 4D 48 89 5C 24"; } @@ -47,46 +53,10 @@ public class Plugin : IDalamudPlugin { var yaml = await resp.Content.ReadAsStringAsync(); #endif - var de = new DeserializerBuilder() - .WithNamingConvention(UnderscoredNamingConvention.Instance) - .Build(); - await this.Mutex.WaitAsync(); try { - this.Data = de.Deserialize(yaml); - foreach (var replacement in this.Data.Replacements) { - var lines = replacement.Text - .ReplaceLineEndings("\n") - .Split('\n'); - var seStringBuilder = new SeStringBuilder(); - for (var i = 0; i < lines.Length; i++) { - if (i != 0) { - seStringBuilder.Add(NewLinePayload.Payload); - } - - seStringBuilder.AddText(lines[i].TrimEnd()); - } - - var textBytes = seStringBuilder.Encode(); - this.Log!.Info(Convert.ToHexString(textBytes)); - - unsafe { - var ptr = (uint*) Marshal.AllocHGlobal(8 + textBytes.Length + 1); - try { - *ptr = 8; - *(ptr + 1) = replacement.Slowly ? 1u : 0u; - - var bytes = (byte*) (ptr + 2); - bytes[textBytes.Length] = 0; - Marshal.Copy(textBytes, 0, (nint) bytes, textBytes.Length); - - this.ReplacementPointers![replacement.Id] = (nint) ptr; - } catch (Exception ex) { - this.Log!.Error(ex, "Error serialising balloon message"); - Marshal.FreeHGlobal((nint) ptr); - } - } - } + this.Data = Plugin.Deserializer.Deserialize(yaml); + this.ReplacementPointers.Clear(); } finally { this.Mutex.Release(); } @@ -105,22 +75,80 @@ public class Plugin : IDalamudPlugin { } private nint GetBalloonRowDetour(uint rowId) { - if (this.ReplacementPointers.TryGetValue(rowId, out var ptr)) { - return ptr; + try { + var ptr = this.GetBalloonRowDetourInner(rowId); + if (ptr != null) { + return ptr.Value; + } + } catch (Exception ex) { + this.Log.Error(ex, "Error in GetBalloonRowDetour"); } return this.GetBalloonRowHook!.Original(rowId); } + + private nint? GetBalloonRowDetourInner(uint rowId) { + this.Mutex.Wait(); + using var release = new OnDispose(() => this.Mutex.Release()); + + var rep = this.Data.Replacements.FirstOrDefault(rep => rep.Id == rowId); + if (rep == null) { + return null; + } + + var when = rep.When.First(when => when.IsValid(this)); + if (when == null) { + if (rep.Text != null) { + return this.GetOrCreateReplacementPointer(rep.Text, rep.Slowly); + } + + return null; + } + + return this.GetOrCreateReplacementPointer(when.Text, when.Slowly); + } + + private nint GetOrCreateReplacementPointer(string text, bool slowly) { + if (this.ReplacementPointers.TryGetValue((text, slowly), out var cached)) { + return cached; + } + + var lines = text + .ReplaceLineEndings("\n") + .Split('\n'); + var seStringBuilder = new SeStringBuilder(); + for (var i = 0; i < lines.Length; i++) { + if (i != 0) { + seStringBuilder.Add(NewLinePayload.Payload); + } + + seStringBuilder.AddText(lines[i].TrimEnd()); + } + + var textBytes = seStringBuilder.Encode(); + this.Log!.Info(Convert.ToHexString(textBytes)); + + unsafe { + var ptr = (uint*) Marshal.AllocHGlobal(8 + textBytes.Length + 1); + try { + *ptr = 8; + *(ptr + 1) = slowly ? 1u : 0u; + + var bytes = (byte*) (ptr + 2); + bytes[textBytes.Length] = 0; + Marshal.Copy(textBytes, 0, (nint) bytes, textBytes.Length); + + this.ReplacementPointers![(text, slowly)] = (nint) ptr; + return (nint) ptr; + } catch { + Marshal.FreeHGlobal((nint) ptr); + throw; + } + } + } } [Serializable] internal class DataFile { public Replacement[] Replacements { get; init; } } - -[Serializable] -internal class Replacement { - public uint Id { get; init; } - public string Text { get; init; } - public bool Slowly { get; init; } -} diff --git a/TimePasses.csproj b/TimePasses.csproj index 6d4ad2b..affa03f 100644 --- a/TimePasses.csproj +++ b/TimePasses.csproj @@ -45,6 +45,10 @@ $(DalamudLibPath)\ImGui.NET.dll false + + $(DalamudLibPath)\FFXIVClientStructs.dll + false + diff --git a/Util/OnDispose.cs b/Util/OnDispose.cs new file mode 100644 index 0000000..e4a6695 --- /dev/null +++ b/Util/OnDispose.cs @@ -0,0 +1,19 @@ +namespace TimePasses.Model; + +internal class OnDispose : IDisposable { + private bool _disposed; + private readonly Action _action; + + internal OnDispose(Action action) { + this._action = action; + } + + public void Dispose() { + if (this._disposed) { + return; + } + + this._disposed = true; + this._action(); + } +} diff --git a/replacements.yaml b/replacements.yaml index 1b4b7c6..1865bcc 100644 --- a/replacements.yaml +++ b/replacements.yaml @@ -1,5 +1,10 @@ replacements: + # Original: Just what we need. Another outsider. - id: 22 - text: |- - It's been - quiet lately. + when: + - quest: + id: 70058 # final msq for A Realm Reborn + status: complete + text: |- + It's been + quiet lately.