2024-06-17 16:04:57 +00:00
|
|
|
|
using System.Runtime.InteropServices;
|
2024-06-19 14:20:18 +00:00
|
|
|
|
using System.Text;
|
2024-06-17 16:47:39 +00:00
|
|
|
|
using Dalamud.Game.Text.SeStringHandling;
|
|
|
|
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
2024-06-17 16:04:57 +00:00
|
|
|
|
using Dalamud.Hooking;
|
|
|
|
|
using Dalamud.IoC;
|
|
|
|
|
using Dalamud.Plugin;
|
|
|
|
|
using Dalamud.Plugin.Services;
|
|
|
|
|
using Dalamud.Utility.Signatures;
|
2024-06-17 17:25:46 +00:00
|
|
|
|
using TimePasses.Model;
|
2024-06-19 14:30:45 +00:00
|
|
|
|
using TimePasses.Util;
|
2024-06-17 16:04:57 +00:00
|
|
|
|
using YamlDotNet.Serialization;
|
|
|
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
|
|
|
|
|
|
namespace TimePasses;
|
|
|
|
|
|
2024-06-19 14:20:18 +00:00
|
|
|
|
public sealed class Plugin : IDalamudPlugin {
|
2024-06-17 16:04:57 +00:00
|
|
|
|
[PluginService]
|
2024-06-17 19:04:13 +00:00
|
|
|
|
internal static IPluginLog Log { get; set; }
|
2024-06-17 16:04:57 +00:00
|
|
|
|
|
|
|
|
|
[PluginService]
|
2024-06-17 19:04:13 +00:00
|
|
|
|
internal IClientState ClientState { get; init; }
|
|
|
|
|
|
|
|
|
|
[PluginService]
|
|
|
|
|
private IGameInteropProvider GameInterop { get; init; }
|
2024-06-17 16:04:57 +00:00
|
|
|
|
|
|
|
|
|
private HttpClient Client { get; } = new();
|
|
|
|
|
|
|
|
|
|
private DataFile Data { get; set; }
|
2024-06-17 17:25:46 +00:00
|
|
|
|
private Dictionary<(string, bool), nint> ReplacementPointers { get; } = [];
|
2024-06-17 16:04:57 +00:00
|
|
|
|
private SemaphoreSlim Mutex { get; } = new(1, 1);
|
|
|
|
|
|
2024-06-17 18:40:28 +00:00
|
|
|
|
private static IDeserializer Deserializer { get; } = new DeserializerBuilder()
|
2024-06-17 17:25:46 +00:00
|
|
|
|
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
2024-06-17 19:04:13 +00:00
|
|
|
|
.WithTypeConverter(new WhenLevelConverter())
|
2024-06-21 15:27:36 +00:00
|
|
|
|
.WithTypeConverter(ReplacementTextConverter.Instance)
|
2024-06-17 18:28:40 +00:00
|
|
|
|
.WithNodeDeserializer(new WhenNodeDeserialiser())
|
2024-06-17 18:40:28 +00:00
|
|
|
|
.IgnoreUnmatchedProperties()
|
2024-06-17 17:25:46 +00:00
|
|
|
|
.Build();
|
|
|
|
|
|
2024-06-17 16:04:57 +00:00
|
|
|
|
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() {
|
2024-06-17 16:47:39 +00:00
|
|
|
|
this.GameInterop!.InitializeFromAttributes(this);
|
|
|
|
|
this.GetBalloonRowHook?.Enable();
|
|
|
|
|
|
2024-06-17 16:04:57 +00:00
|
|
|
|
Task.Run(async () => {
|
2024-06-19 14:45:41 +00:00
|
|
|
|
string yaml;
|
2024-06-17 16:04:57 +00:00
|
|
|
|
#if DEBUG
|
2024-06-19 14:45:41 +00:00
|
|
|
|
yaml = await Plugin.LoadEmbeddedReplacements();
|
2024-06-17 16:04:57 +00:00
|
|
|
|
#else
|
2024-06-19 14:45:41 +00:00
|
|
|
|
try {
|
|
|
|
|
using var resp = await this.Client.GetAsync("https://git.anna.lgbt/anna/TimePasses/raw/branch/main/replacements.yaml");
|
|
|
|
|
yaml = await resp.Content.ReadAsStringAsync();
|
|
|
|
|
} catch (Exception ex) {
|
|
|
|
|
Plugin.Log.Warning(ex, "could not download replacements");
|
|
|
|
|
yaml = await Plugin.LoadEmbeddedReplacements();
|
|
|
|
|
}
|
2024-06-17 16:04:57 +00:00
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
await this.Mutex.WaitAsync();
|
|
|
|
|
try {
|
2024-06-17 17:25:46 +00:00
|
|
|
|
this.Data = Plugin.Deserializer.Deserialize<DataFile>(yaml);
|
2024-06-19 14:20:18 +00:00
|
|
|
|
this.ResetReplacementPointers();
|
2024-06-17 16:04:57 +00:00
|
|
|
|
} finally {
|
|
|
|
|
this.Mutex.Release();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Dispose() {
|
2024-06-17 16:47:39 +00:00
|
|
|
|
this.GetBalloonRowHook?.Dispose();
|
2024-06-17 16:04:57 +00:00
|
|
|
|
this.Client.Dispose();
|
|
|
|
|
this.Mutex.Dispose();
|
2024-06-19 14:20:18 +00:00
|
|
|
|
this.ResetReplacementPointers();
|
2024-06-17 16:04:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-19 14:46:20 +00:00
|
|
|
|
private static async Task<string> LoadEmbeddedReplacements() {
|
2024-06-19 14:45:41 +00:00
|
|
|
|
using var stream = typeof(Plugin).Assembly.GetManifestResourceStream("TimePasses.replacements.yaml");
|
|
|
|
|
using var reader = new StreamReader(stream!);
|
|
|
|
|
return await reader.ReadToEndAsync();
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-17 16:04:57 +00:00
|
|
|
|
private nint GetBalloonRowDetour(uint rowId) {
|
2024-06-17 17:25:46 +00:00
|
|
|
|
try {
|
|
|
|
|
var ptr = this.GetBalloonRowDetourInner(rowId);
|
|
|
|
|
if (ptr != null) {
|
|
|
|
|
return ptr.Value;
|
|
|
|
|
}
|
|
|
|
|
} catch (Exception ex) {
|
2024-06-17 17:41:16 +00:00
|
|
|
|
Plugin.Log.Error(ex, "Error in GetBalloonRowDetour");
|
2024-06-17 16:04:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.GetBalloonRowHook!.Original(rowId);
|
|
|
|
|
}
|
2024-06-17 17:25:46 +00:00
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-17 17:41:16 +00:00
|
|
|
|
var when = rep.When.FirstOrDefault(when => when.IsValid(this));
|
2024-06-19 14:30:45 +00:00
|
|
|
|
if (when != null) {
|
2024-06-21 15:27:36 +00:00
|
|
|
|
var idx = Random.Shared.Next(when.Text.Count);
|
|
|
|
|
return this.GetOrCreateReplacementPointer(when.Text[idx], when.Slowly);
|
2024-06-19 14:30:45 +00:00
|
|
|
|
}
|
2024-06-17 17:25:46 +00:00
|
|
|
|
|
2024-06-19 14:30:45 +00:00
|
|
|
|
if (rep.Text != null) {
|
2024-06-21 15:27:36 +00:00
|
|
|
|
var idx = Random.Shared.Next(rep.Text.Count);
|
|
|
|
|
return this.GetOrCreateReplacementPointer(rep.Text[idx], rep.Slowly);
|
2024-06-17 17:25:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-19 14:30:45 +00:00
|
|
|
|
return null;
|
2024-06-17 17:25:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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');
|
2024-06-19 14:20:18 +00:00
|
|
|
|
var sb = new StringBuilder();
|
2024-06-17 17:25:46 +00:00
|
|
|
|
var seStringBuilder = new SeStringBuilder();
|
|
|
|
|
for (var i = 0; i < lines.Length; i++) {
|
|
|
|
|
if (i != 0) {
|
|
|
|
|
seStringBuilder.Add(NewLinePayload.Payload);
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-19 14:20:18 +00:00
|
|
|
|
var line = lines[i]
|
|
|
|
|
.TrimEnd()
|
|
|
|
|
.Replace("<em>", Markers.EmphasisOn.ToString())
|
|
|
|
|
.Replace("</em>", Markers.EmphasisOff.ToString());
|
|
|
|
|
|
2024-06-19 14:28:01 +00:00
|
|
|
|
foreach (var ch in line) {
|
2024-06-19 14:20:18 +00:00
|
|
|
|
switch (ch) {
|
|
|
|
|
case Markers.EmphasisOn: {
|
|
|
|
|
Append();
|
|
|
|
|
seStringBuilder.AddItalicsOn();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case Markers.EmphasisOff: {
|
|
|
|
|
Append();
|
|
|
|
|
seStringBuilder.AddItalicsOff();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default: {
|
|
|
|
|
sb.Append(ch);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Append();
|
2024-06-19 14:28:01 +00:00
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
void Append() {
|
|
|
|
|
if (sb.Length == 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
seStringBuilder.AddText(sb.ToString());
|
|
|
|
|
sb.Clear();
|
|
|
|
|
}
|
2024-06-17 17:25:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
2024-06-19 14:30:45 +00:00
|
|
|
|
this.ReplacementPointers[(text, slowly)] = (nint) ptr;
|
2024-06-17 17:25:46 +00:00
|
|
|
|
return (nint) ptr;
|
|
|
|
|
} catch {
|
|
|
|
|
Marshal.FreeHGlobal((nint) ptr);
|
|
|
|
|
throw;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-19 14:20:18 +00:00
|
|
|
|
|
|
|
|
|
private void ResetReplacementPointers() {
|
|
|
|
|
foreach (var (_, ptr) in this.ReplacementPointers) {
|
|
|
|
|
Marshal.FreeHGlobal(ptr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.ReplacementPointers.Clear();
|
|
|
|
|
}
|
2024-06-17 16:04:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
internal class DataFile {
|
2024-06-17 18:40:28 +00:00
|
|
|
|
public Definitions Definitions { get; init; }
|
2024-06-17 16:04:57 +00:00
|
|
|
|
public Replacement[] Replacements { get; init; }
|
|
|
|
|
}
|
2024-06-17 18:40:28 +00:00
|
|
|
|
|
|
|
|
|
[Serializable]
|
|
|
|
|
public class Definitions {
|
2024-06-18 15:12:18 +00:00
|
|
|
|
public uint[] Quests { get; init; }
|
2024-06-17 18:40:28 +00:00
|
|
|
|
}
|
2024-06-19 14:20:18 +00:00
|
|
|
|
|
|
|
|
|
internal static class Markers {
|
|
|
|
|
internal const char EmphasisOn = '\uf000';
|
|
|
|
|
internal const char EmphasisOff = '\uf001';
|
|
|
|
|
}
|