feat: add conditional replacements
This commit is contained in:
parent
cd47b171f7
commit
7b1cbbd8f7
40
Model/IWhen.cs
Normal file
40
Model/IWhen.cs
Normal 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
9
Model/Replacement.cs
Normal 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
22
Model/WhenQuest.cs
Normal 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
6
Model/WhenStatus.cs
Normal file
@ -0,0 +1,6 @@
|
||||
[Serializable]
|
||||
public enum QuestStatus {
|
||||
Complete,
|
||||
Incomplete,
|
||||
InProgress,
|
||||
}
|
124
Plugin.cs
124
Plugin.cs
@ -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; }
|
||||
}
|
||||
|
@ -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
19
Util/OnDispose.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user