Skip to content

Commit

Permalink
Update Lumina and use MacroStringParser (#2033)
Browse files Browse the repository at this point in the history
No longer requires hook and main thread requirements on compiling macro
strings.

Needs lookup table fixing on Lumina; using reflection to fix for the
time being.
  • Loading branch information
Soreepeong committed Sep 14, 2024
1 parent 4b4227d commit 1f74293
Show file tree
Hide file tree
Showing 5 changed files with 40 additions and 123 deletions.
2 changes: 1 addition & 1 deletion Dalamud.CorePlugin/Dalamud.CorePlugin.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Lumina" Version="4.1.1" />
<PackageReference Include="Lumina" Version="4.2.1" />
<PackageReference Include="Lumina.Excel" Version="7.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.333">
Expand Down
2 changes: 1 addition & 1 deletion Dalamud/Dalamud.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
<PackageReference Include="goaaats.Reloaded.Hooks" Version="4.2.0-goat.4" />
<PackageReference Include="goaaats.Reloaded.Assembler" Version="1.0.14-goat.2" />
<PackageReference Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageReference Include="Lumina" Version="4.1.1" />
<PackageReference Include="Lumina" Version="4.2.1" />
<PackageReference Include="Lumina.Excel" Version="7.0.1" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="9.0.0-preview.1.24081.5" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.3.46-beta">
Expand Down
24 changes: 13 additions & 11 deletions Dalamud/Game/Text/SeStringHandling/Payloads/DalamudLinkPayload.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,19 @@ public override string ToString() =>
/// <inheritdoc/>
protected override byte[] EncodeImpl()
{
return new Lumina.Text.SeStringBuilder()
.BeginMacro(MacroCode.Link)
.AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1)
.AppendUIntExpression(this.CommandId)
.AppendIntExpression(this.Extra1)
.AppendIntExpression(this.Extra2)
.BeginStringExpression()
.Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString }))
.EndExpression()
.EndMacro()
.ToArray();
var ssb = Lumina.Text.SeStringBuilder.SharedPool.Get();
var res = ssb.BeginMacro(MacroCode.Link)
.AppendIntExpression((int)EmbeddedInfoType.DalamudLink - 1)
.AppendUIntExpression(this.CommandId)
.AppendIntExpression(this.Extra1)
.AppendIntExpression(this.Extra2)
.BeginStringExpression()
.Append(JsonConvert.SerializeObject(new[] { this.Plugin, this.ExtraString }))
.EndExpression()
.EndMacro()
.ToArray();
Lumina.Text.SeStringBuilder.SharedPool.Return(ssb);
return res;
}

/// <inheritdoc/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
Expand All @@ -16,18 +15,16 @@

using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;

using ImGuiNET;

using Lumina.Excel.GeneratedSheets2;
using Lumina.Text.Parse;
using Lumina.Text.Payloads;
using Lumina.Text.ReadOnly;

using static Dalamud.Game.Text.SeStringHandling.BitmapFontIcon;

using SeStringBuilder = Lumina.Text.SeStringBuilder;

namespace Dalamud.Interface.ImGuiSeStringRenderer.Internal;

/// <summary>Draws SeString.</summary>
Expand All @@ -46,30 +43,12 @@ internal unsafe class SeStringRenderer : IInternalDisposableService
/// of this placeholder. On its own, usually displayed like <c>[OBJ]</c>.</summary>
private const int ObjectReplacementCharacter = '\uFFFC';

/// <summary>SeString to return instead, if macro encoder has failed and could not provide us the reason.</summary>
private static readonly ReadOnlySeString MacroEncoderEncodeStringError =
new SeStringBuilder()
.BeginMacro(MacroCode.ColorType).AppendIntExpression(508).EndMacro()
.BeginMacro(MacroCode.EdgeColorType).AppendIntExpression(509).EndMacro()
.Append(
"<encode failed, and error message generation failed because the part that caused the error was too long>"u8)
.BeginMacro(MacroCode.EdgeColorType).AppendIntExpression(0).EndMacro()
.BeginMacro(MacroCode.ColorType).AppendIntExpression(0).EndMacro()
.ToReadOnlySeString();

[ServiceManager.ServiceDependency]
private readonly GameConfig gameConfig = Service<GameConfig>.Get();

/// <summary>Cache of compiled SeStrings from <see cref="CompileAndCache"/>.</summary>
private readonly ConcurrentLru<string, ReadOnlySeString> cache = new(1024);

/// <summary>Sets the global invalid parameter handler. Used to suppress <c>vsprintf_s</c> from raising.</summary>
/// <remarks>There exists a thread local version of this, but as the game-provided implementation is what
/// effectively is a screaming tool that the game has a bug, it should be safe to fail in any means.</remarks>
private readonly delegate* unmanaged<
delegate* unmanaged<char*, char*, char*, int, nuint, void>,
delegate* unmanaged<char*, char*, char*, int, nuint, void>> setInvalidParameterHandler;

/// <summary>Parsed <c>gfdata.gfd</c> file, containing bitmap font icon lookup table.</summary>
private readonly GfdFile gfd;

Expand All @@ -90,14 +69,6 @@ private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
this.colorStackSet = new(
dm.Excel.GetSheet<UIColor>() ?? throw new InvalidOperationException("Failed to access UIColor sheet."));
this.gfd = dm.GetFile<GfdFile>("common/font/gfdata.gfd")!;

// SetUnhandledExceptionFilter(who cares);
// _set_purecall_handler(() => *(int*)0 = 0xff14);
// _set_invalid_parameter_handler(() => *(int*)0 = 0xff14);
var f = sigScanner.ScanText(
"ff 15 ?? ?? ?? ?? 48 8d 0d ?? ?? ?? ?? e8 ?? ?? ?? ?? 48 8d 0d ?? ?? ?? ?? e8 ?? ?? ?? ??") + 26;
fixed (void* p = &this.setInvalidParameterHandler)
*(nint*)p = *(int*)f + f + 4;
}

/// <summary>Finalizes an instance of the <see cref="SeStringRenderer"/> class.</summary>
Expand All @@ -106,72 +77,16 @@ private SeStringRenderer(DataManager dm, TargetSigScanner sigScanner)
/// <inheritdoc/>
void IInternalDisposableService.DisposeService() => this.ReleaseUnmanagedResources();

/// <summary>Compiles a SeString from a text macro representation.</summary>
/// <param name="text">SeString text macro representation.</param>
/// <returns>Compiled SeString.</returns>
public ReadOnlySeString Compile(ReadOnlySpan<byte> text)
{
// MacroEncoder looks stateful; disallowing calls from off main threads for now.
ThreadSafety.AssertMainThread();

var prev = this.setInvalidParameterHandler(&MsvcrtInvalidParameterHandlerDetour);
try
{
using var tmp = new Utf8String();
RaptureTextModule.Instance()->MacroEncoder.EncodeString(&tmp, text);
return new(tmp.AsSpan().ToArray());
}
catch (Exception)
{
return MacroEncoderEncodeStringError;
}
finally
{
this.setInvalidParameterHandler(prev);
}

[UnmanagedCallersOnly]
static void MsvcrtInvalidParameterHandlerDetour(char* a, char* b, char* c, int d, nuint e) =>
throw new InvalidOperationException();
}

/// <summary>Compiles a SeString from a text macro representation.</summary>
/// <param name="text">SeString text macro representation.</param>
/// <returns>Compiled SeString.</returns>
public ReadOnlySeString Compile(ReadOnlySpan<char> text)
{
var len = Encoding.UTF8.GetByteCount(text);
if (len >= 1024)
{
var buf = ArrayPool<byte>.Shared.Rent(len + 1);
buf[Encoding.UTF8.GetBytes(text, buf)] = 0;
var res = this.Compile(buf);
ArrayPool<byte>.Shared.Return(buf);
return res;
}
else
{
Span<byte> buf = stackalloc byte[len + 1];
buf[Encoding.UTF8.GetBytes(text, buf)] = 0;
return this.Compile(buf);
}
}

/// <summary>Compiles and caches a SeString from a text macro representation.</summary>
/// <param name="text">SeString text macro representation.
/// Newline characters will be normalized to newline payloads.</param>
/// <returns>Compiled SeString.</returns>
public ReadOnlySeString CompileAndCache(string text)
{
// MacroEncoder looks stateful; disallowing calls from off main threads for now.
// Note that this is replicated in context.Compile. Only access cache from the main thread.
ThreadSafety.AssertMainThread();

return this.cache.GetOrAdd(
public ReadOnlySeString CompileAndCache(string text) =>
this.cache.GetOrAdd(
text,
static (text, context) => context.Compile(text.ReplaceLineEndings("<br>")),
this);
}
static text => ReadOnlySeString.FromMacroString(
text.ReplaceLineEndings("<br>"),
new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError }));

/// <summary>Compiles and caches a SeString from a text macro representation, and then draws it.</summary>
/// <param name="text">SeString text macro representation.
Expand Down
38 changes: 19 additions & 19 deletions Dalamud/Utility/SeStringExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Dalamud.Interface.ImGuiSeStringRenderer.Internal;
using Lumina.Text.Parse;

using DSeString = Dalamud.Game.Text.SeStringHandling.SeString;
using DSeStringBuilder = Dalamud.Game.Text.SeStringHandling.SeStringBuilder;
Expand All @@ -24,44 +24,44 @@ public static class SeStringExtensions
/// <param name="ssb">Target SeString builder.</param>
/// <param name="macroString">Macro string in UTF-8 to compile and append to <paramref name="ssb"/>.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Must be called from the main thread.</remarks>
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<byte> macroString)
{
ThreadSafety.AssertMainThread();
return ssb.Append(Service<SeStringRenderer>.Get().Compile(macroString));
}
[Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.")]
[Api11ToDo("Remove")]
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<byte> macroString) =>
ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError });

/// <summary>Compiles and appends a macro string.</summary>
/// <param name="ssb">Target SeString builder.</param>
/// <param name="macroString">Macro string in UTF-16 to compile and append to <paramref name="ssb"/>.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Must be called from the main thread.</remarks>
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<char> macroString)
{
ThreadSafety.AssertMainThread();
return ssb.Append(Service<SeStringRenderer>.Get().Compile(macroString));
}
[Obsolete($"Use {nameof(LSeStringBuilder)}.{nameof(LSeStringBuilder.AppendMacroString)} directly instead.")]
[Api11ToDo("Remove")]
public static LSeStringBuilder AppendMacroString(this LSeStringBuilder ssb, ReadOnlySpan<char> macroString) =>
ssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError });

/// <summary>Compiles and appends a macro string.</summary>
/// <param name="ssb">Target SeString builder.</param>
/// <param name="macroString">Macro string in UTF-8 to compile and append to <paramref name="ssb"/>.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Must be called from the main thread.</remarks>
public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan<byte> macroString)
{
ThreadSafety.AssertMainThread();
return ssb.Append(DSeString.Parse(Service<SeStringRenderer>.Get().Compile(macroString)));
var lssb = LSeStringBuilder.SharedPool.Get();
lssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError });
ssb.Append(DSeString.Parse(lssb.ToReadOnlySeString().Data.Span));
LSeStringBuilder.SharedPool.Return(lssb);
return ssb;
}

/// <summary>Compiles and appends a macro string.</summary>
/// <param name="ssb">Target SeString builder.</param>
/// <param name="macroString">Macro string in UTF-16 to compile and append to <paramref name="ssb"/>.</param>
/// <returns><c>this</c> for method chaining.</returns>
/// <remarks>Must be called from the main thread.</remarks>
public static DSeStringBuilder AppendMacroString(this DSeStringBuilder ssb, ReadOnlySpan<char> macroString)
{
ThreadSafety.AssertMainThread();
return ssb.Append(DSeString.Parse(Service<SeStringRenderer>.Get().Compile(macroString)));
var lssb = LSeStringBuilder.SharedPool.Get();
lssb.AppendMacroString(macroString, new() { ExceptionMode = MacroStringParseExceptionMode.EmbedError });
ssb.Append(DSeString.Parse(lssb.ToReadOnlySeString().Data.Span));
LSeStringBuilder.SharedPool.Return(lssb);
return ssb;
}

/// <summary>
Expand Down

0 comments on commit 1f74293

Please sign in to comment.