TimePasses/Plugin.cs

161 lines
5.0 KiB
C#

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 TimePasses.Model;
using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions;
namespace TimePasses;
public class Plugin : IDalamudPlugin {
[PluginService]
private IGameInteropProvider GameInterop { get; init; }
[PluginService]
internal static IPluginLog Log { get; set; }
private HttpClient Client { get; } = new();
private DataFile Data { get; set; }
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())
.WithTypeDiscriminatingNodeDeserializer(o => {
var keyMappings = new Dictionary<string, Type> {
["quest"] = typeof(WhenQuest),
};
o.AddUniqueKeyTypeDiscriminator<IWhen>(keyMappings);
})
.Build();
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() {
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
await this.Mutex.WaitAsync();
try {
this.Data = Plugin.Deserializer.Deserialize<DataFile>(yaml);
this.ReplacementPointers.Clear();
} 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) {
try {
var ptr = this.GetBalloonRowDetourInner(rowId);
if (ptr != null) {
return ptr.Value;
}
} catch (Exception ex) {
Plugin.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.FirstOrDefault(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();
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; }
}