using System.Runtime.InteropServices; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.Plugin; using Dalamud.Plugin.Services; using Dalamud.Utility.Signatures; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace TimePasses; public class Plugin : IDalamudPlugin { [PluginService] private IGameInteropProvider GameInterop { get; init; } [PluginService] private IPluginLog Log { get; init; } private HttpClient Client { get; } = new(); private DataFile Data { get; set; } private Dictionary ReplacementPointers { get; } = []; private SemaphoreSlim Mutex { get; } = new(1, 1); private static class Signatures { internal const string GetBalloonRow = "E8 ?? ?? ?? ?? 48 85 C0 74 4D 48 89 5C 24"; } private delegate nint GetBalloonRowDelegate(uint rowId); [Signature(Signatures.GetBalloonRow, DetourName = nameof(GetBalloonRowDetour))] private Hook? GetBalloonRowHook { get; init; } public Plugin() { this.GameInterop!.InitializeFromAttributes(this); this.GetBalloonRowHook?.Enable(); Task.Run(async () => { #if DEBUG var stream = typeof(Plugin).Assembly.GetManifestResourceStream("TimePasses.replacements.yaml"); using var reader = new StreamReader(stream!); var yaml = await reader.ReadToEndAsync(); #else using var resp = await this.Client.GetAsync("https://git.anna.lgbt/anna/TimePasses/raw/branch/main/replacements.yaml"); 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); } } } } finally { this.Mutex.Release(); } }); } public void Dispose() { this.GetBalloonRowHook?.Dispose(); this.Client.Dispose(); this.Mutex.Dispose(); foreach (var (_, ptr) in this.ReplacementPointers) { Marshal.FreeHGlobal(ptr); } this.ReplacementPointers.Clear(); } private nint GetBalloonRowDetour(uint rowId) { if (this.ReplacementPointers.TryGetValue(rowId, out var ptr)) { return ptr; } return this.GetBalloonRowHook!.Original(rowId); } } [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; } }