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;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
|
using TimePasses.Model;
|
||||||
using YamlDotNet.Serialization;
|
using YamlDotNet.Serialization;
|
||||||
using YamlDotNet.Serialization.NamingConventions;
|
using YamlDotNet.Serialization.NamingConventions;
|
||||||
|
|
||||||
@ -21,9 +22,14 @@ public class Plugin : IDalamudPlugin {
|
|||||||
private HttpClient Client { get; } = new();
|
private HttpClient Client { get; } = new();
|
||||||
|
|
||||||
private DataFile Data { get; set; }
|
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);
|
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 {
|
private static class Signatures {
|
||||||
internal const string GetBalloonRow = "E8 ?? ?? ?? ?? 48 85 C0 74 4D 48 89 5C 24";
|
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();
|
var yaml = await resp.Content.ReadAsStringAsync();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
var de = new DeserializerBuilder()
|
|
||||||
.WithNamingConvention(UnderscoredNamingConvention.Instance)
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
await this.Mutex.WaitAsync();
|
await this.Mutex.WaitAsync();
|
||||||
try {
|
try {
|
||||||
this.Data = de.Deserialize<DataFile>(yaml);
|
this.Data = Plugin.Deserializer.Deserialize<DataFile>(yaml);
|
||||||
foreach (var replacement in this.Data.Replacements) {
|
this.ReplacementPointers.Clear();
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
this.Mutex.Release();
|
this.Mutex.Release();
|
||||||
}
|
}
|
||||||
@ -105,22 +75,80 @@ public class Plugin : IDalamudPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private nint GetBalloonRowDetour(uint rowId) {
|
private nint GetBalloonRowDetour(uint rowId) {
|
||||||
if (this.ReplacementPointers.TryGetValue(rowId, out var ptr)) {
|
try {
|
||||||
return ptr;
|
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);
|
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]
|
[Serializable]
|
||||||
internal class DataFile {
|
internal class DataFile {
|
||||||
public Replacement[] Replacements { get; init; }
|
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>
|
<HintPath>$(DalamudLibPath)\ImGui.NET.dll</HintPath>
|
||||||
<Private>false</Private>
|
<Private>false</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="FFXIVClientStructs">
|
||||||
|
<HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath>
|
||||||
|
<Private>false</Private>
|
||||||
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<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:
|
replacements:
|
||||||
|
# Original: Just what we need. Another outsider.
|
||||||
- id: 22
|
- id: 22
|
||||||
|
when:
|
||||||
|
- quest:
|
||||||
|
id: 70058 # final msq for A Realm Reborn
|
||||||
|
status: complete
|
||||||
text: |-
|
text: |-
|
||||||
It's been
|
It's been
|
||||||
quiet lately.
|
quiet lately.
|
||||||
|
Loading…
Reference in New Issue
Block a user