TimePasses/Plugin.cs
2024-06-17 12:40:09 -04:00

110 lines
3.6 KiB
C#

using System.Runtime.InteropServices;
using System.Text;
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<uint, nint> ReplacementPointers { get; set; }
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<GetBalloonRowDelegate>? GetBalloonRowHook { get; init; }
public Plugin() {
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<DataFile>(yaml);
foreach (var replacement in this.Data.Replacements) {
var replaced = replacement.Text.ReplaceLineEndings("\x02\x10\x01\x03");
var textBytes = Encoding.UTF8.GetBytes(replaced);
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.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; }
}