feat: add conditional replacements

This commit is contained in:
Anna 2024-06-17 13:25:46 -04:00
parent cd47b171f7
commit 7b1cbbd8f7
Signed by: anna
GPG Key ID: D0943384CD9F87D1
8 changed files with 184 additions and 51 deletions

40
Model/IWhen.cs Normal file
View File

@ -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<MappingStart>();
var name = parser.Consume<Scalar>();
if (!name.IsKey) {
throw new YamlException("invalid when: missing key");
}
switch (name.Value) {
case "quest": {
return Plugin.Deserializer.Deserialize<WhenQuest>(parser);
}
default: {
throw new YamlException($"invalid when: unknown type {name.Value}");
}
}
}
public void WriteYaml(IEmitter emitter, object? value, Type type) {
throw new NotImplementedException();
}
}

9
Model/Replacement.cs Normal file
View File

@ -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; } = [];
}

22
Model/WhenQuest.cs Normal file
View File

@ -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,
};
}
}

6
Model/WhenStatus.cs Normal file
View File

@ -0,0 +1,6 @@
[Serializable]
public enum QuestStatus {
Complete,
Incomplete,
InProgress,
}

124
Plugin.cs
View File

@ -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<uint, nint> 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<DataFile>(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<DataFile>(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; }
}

View File

@ -45,6 +45,10 @@
<HintPath>$(DalamudLibPath)\ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<ItemGroup>

19
Util/OnDispose.cs Normal file
View File

@ -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();
}
}

View File

@ -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.