feat: handle message queue delay and fix wrapping
Make message wrapping work with non-English languages. Special-case /tell wrapping to send to the correct recipient. Add an artificial wait between sending messages in the queue, longer for public commands to prevent rate-limiting.
This commit is contained in:
parent
e41b90d63e
commit
c092bf2019
|
@ -6,6 +6,7 @@
|
||||||
<InputAssemblies Include="$(OutputPath)\*.dll"
|
<InputAssemblies Include="$(OutputPath)\*.dll"
|
||||||
Exclude="$(OutputPath)\$(AssemblyName).dll;
|
Exclude="$(OutputPath)\$(AssemblyName).dll;
|
||||||
$(OutputPath)\libsodium.dll;
|
$(OutputPath)\libsodium.dll;
|
||||||
|
$(OutputPath)\xivchat_native_tools.dll;
|
||||||
$(OutputPath)\System.Buffers.dll;
|
$(OutputPath)\System.Buffers.dll;
|
||||||
$(OutputPath)\System.Memory.dll;
|
$(OutputPath)\System.Memory.dll;
|
||||||
$(OutputPath)\System.Numerics.Vectors.dll"/>
|
$(OutputPath)\System.Numerics.Vectors.dll"/>
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace XIVChatPlugin {
|
||||||
|
public static unsafe class NativeTools {
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private readonly struct RawVec {
|
||||||
|
public readonly byte** pointer;
|
||||||
|
public readonly uint length;
|
||||||
|
private readonly uint capacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("xivchat_native_tools.dll")]
|
||||||
|
private static extern RawVec* wrap(byte* input, uint width);
|
||||||
|
|
||||||
|
[DllImport("xivchat_native_tools.dll")]
|
||||||
|
private static extern void wrap_free(RawVec* raw);
|
||||||
|
|
||||||
|
public static IEnumerable<string> Wrap(string input, uint width) {
|
||||||
|
RawVec* raw;
|
||||||
|
fixed (byte* ptr = Encoding.UTF8.GetBytes(input).Terminate()) {
|
||||||
|
raw = wrap(ptr, width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (raw == null) {
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var strings = new List<string>((int) raw->length);
|
||||||
|
for (var i = 0; i < raw->length; i++) {
|
||||||
|
var bytes = Util.ReadTerminated(raw->pointer[i]);
|
||||||
|
strings.Add(Encoding.UTF8.GetString(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
wrap_free(raw);
|
||||||
|
|
||||||
|
return strings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
|
@ -6,6 +6,7 @@ using Sodium;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
|
@ -23,8 +24,25 @@ using XIVChatCommon.Message.Server;
|
||||||
|
|
||||||
namespace XIVChatPlugin {
|
namespace XIVChatPlugin {
|
||||||
public class Server : IDisposable {
|
public class Server : IDisposable {
|
||||||
|
private const int MaxMessageLength = 500;
|
||||||
|
|
||||||
|
private static readonly string[] PublicPrefixes = {
|
||||||
|
"/t ",
|
||||||
|
"/tell ",
|
||||||
|
"/reply ",
|
||||||
|
"/r ",
|
||||||
|
"/say ",
|
||||||
|
"/s ",
|
||||||
|
"/shout ",
|
||||||
|
"/sh ",
|
||||||
|
"/yell ",
|
||||||
|
"/y ",
|
||||||
|
};
|
||||||
|
|
||||||
private readonly Plugin _plugin;
|
private readonly Plugin _plugin;
|
||||||
|
|
||||||
|
private readonly Stopwatch _sendWatch = new();
|
||||||
|
|
||||||
private readonly CancellationTokenSource _tokenSource = new();
|
private readonly CancellationTokenSource _tokenSource = new();
|
||||||
private readonly ConcurrentQueue<string> _toGame = new();
|
private readonly ConcurrentQueue<string> _toGame = new();
|
||||||
|
|
||||||
|
@ -50,11 +68,13 @@ namespace XIVChatPlugin {
|
||||||
private const int MaxMessageSize = 128_000;
|
private const int MaxMessageSize = 128_000;
|
||||||
|
|
||||||
public Server(Plugin plugin) {
|
public Server(Plugin plugin) {
|
||||||
this._plugin = plugin ?? throw new ArgumentNullException(nameof(plugin), "Plugin cannot be null");
|
this._plugin = plugin;
|
||||||
if (this._plugin.Config.KeyPair == null) {
|
if (this._plugin.Config.KeyPair == null) {
|
||||||
this.RegenerateKeyPair();
|
this.RegenerateKeyPair();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._sendWatch.Start();
|
||||||
|
|
||||||
this._plugin.Functions.ReceiveFriendList += this.OnReceiveFriendList;
|
this._plugin.Functions.ReceiveFriendList += this.OnReceiveFriendList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,7 +214,7 @@ namespace XIVChatPlugin {
|
||||||
|
|
||||||
if (sender.Payloads.Count > 0) {
|
if (sender.Payloads.Count > 0) {
|
||||||
var format = this.FormatFor(chatCode.Type);
|
var format = this.FormatFor(chatCode.Type);
|
||||||
if (format != null && format.IsPresent) {
|
if (format is { IsPresent: true }) {
|
||||||
chunks.Add(new TextChunk(format.Before) {
|
chunks.Add(new TextChunk(format.Before) {
|
||||||
FallbackColour = colour,
|
FallbackColour = colour,
|
||||||
});
|
});
|
||||||
|
@ -251,10 +271,25 @@ namespace XIVChatPlugin {
|
||||||
client.Queue.Writer.TryWrite(new Availability(available));
|
client.Queue.Writer.TryWrite(new Availability(available));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int time;
|
||||||
|
if (this._toGame.TryPeek(out var peek) && PublicPrefixes.Any(prefix => peek.StartsWith(prefix))) {
|
||||||
|
time = 1_000;
|
||||||
|
} else if (this._currentChannel is InputChannel.Tell or InputChannel.Say or InputChannel.Shout or InputChannel.Yell) {
|
||||||
|
time = 1_000;
|
||||||
|
} else {
|
||||||
|
time = 250;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._sendWatch.Elapsed < TimeSpan.FromMilliseconds(time)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this._toGame.TryDequeue(out var message)) {
|
if (!this._toGame.TryDequeue(out var message)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._sendWatch.Restart();
|
||||||
|
|
||||||
this._plugin.Functions.ProcessChatBox(message);
|
this._plugin.Functions.ProcessChatBox(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -575,7 +610,7 @@ namespace XIVChatPlugin {
|
||||||
chunks.Add(new TextChunk(text) {
|
chunks.Add(new TextChunk(text) {
|
||||||
FallbackColour = defaultColour,
|
FallbackColour = defaultColour,
|
||||||
Foreground = foreground.Count > 0 ? foreground.Peek() : (uint?) null,
|
Foreground = foreground.Count > 0 ? foreground.Peek() : (uint?) null,
|
||||||
Glow = glow.Count > 0 ? glow.Peek() : (uint?) null,
|
Glow = glow.Count > 0 ? glow.Peek() : null,
|
||||||
Italic = italic,
|
Italic = italic,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -643,9 +678,7 @@ namespace XIVChatPlugin {
|
||||||
private IEnumerable<ServerMessage> MessagesAfter(DateTime time) => this._backlog.Where(msg => msg.Timestamp > time).ToArray();
|
private IEnumerable<ServerMessage> MessagesAfter(DateTime time) => this._backlog.Where(msg => msg.Timestamp > time).ToArray();
|
||||||
|
|
||||||
private static IEnumerable<string> Wrap(string input) {
|
private static IEnumerable<string> Wrap(string input) {
|
||||||
const int limit = 500;
|
if (input.Length <= MaxMessageLength) {
|
||||||
|
|
||||||
if (input.Length <= limit) {
|
|
||||||
return new[] {
|
return new[] {
|
||||||
input,
|
input,
|
||||||
};
|
};
|
||||||
|
@ -656,59 +689,22 @@ namespace XIVChatPlugin {
|
||||||
var space = input.IndexOf(' ');
|
var space = input.IndexOf(' ');
|
||||||
if (space != -1) {
|
if (space != -1) {
|
||||||
prefix = input.Substring(0, space);
|
prefix = input.Substring(0, space);
|
||||||
input = input.Substring(space + 1);
|
// handle wrapping tells
|
||||||
}
|
if (prefix is "/tell" or "/t") {
|
||||||
}
|
var tellSpace = input.IndexOfCount(' ', 3);
|
||||||
|
if (tellSpace != -1) {
|
||||||
var parts = new List<string>();
|
prefix = input.Substring(0, tellSpace);
|
||||||
|
input = input.Substring(tellSpace + 1);
|
||||||
var builder = new StringBuilder(limit);
|
|
||||||
|
|
||||||
foreach (var word in input.Split(' ')) {
|
|
||||||
if (word.Length > limit) {
|
|
||||||
var wordParts = (int) Math.Ceiling((float) word.Length / limit);
|
|
||||||
for (var i = 0; i < wordParts; i++) {
|
|
||||||
var start = i == 0 ? 0 : (i * limit);
|
|
||||||
var partLength = limit;
|
|
||||||
if (prefix.Length != 0) {
|
|
||||||
start = start == 0 ? 0 : (start - (prefix.Length + 1) * i);
|
|
||||||
partLength = partLength - prefix.Length - 1;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
var part = word.Length - start < partLength ? word.Substring(start) : word.Substring(start, partLength);
|
input = input.Substring(space + 1);
|
||||||
if (part.Length == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prefix.Length != 0) {
|
|
||||||
part = prefix + " " + part;
|
|
||||||
}
|
|
||||||
|
|
||||||
parts.Add(part);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (builder.Length + word.Length > limit) {
|
|
||||||
parts.Add(builder.ToString().TrimEnd(' '));
|
|
||||||
builder.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (builder.Length == 0 && prefix.Length != 0) {
|
|
||||||
builder.Append(prefix);
|
|
||||||
builder.Append(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.Append(word);
|
|
||||||
builder.Append(' ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (builder.Length != 0) {
|
return NativeTools.Wrap(input, MaxMessageLength)
|
||||||
parts.Add(builder.ToString().TrimEnd(' '));
|
.Select(text => $"{prefix} {text}")
|
||||||
}
|
.ToArray();
|
||||||
|
|
||||||
return parts.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BroadcastMessage(Encodable message) {
|
private void BroadcastMessage(Encodable message) {
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace XIVChatPlugin {
|
||||||
|
internal static class Util {
|
||||||
|
public static int IndexOfCount(this string source, char toFind, int position) {
|
||||||
|
var index = -1;
|
||||||
|
for (var i = 0; i < position; i++) {
|
||||||
|
index = source.IndexOf(toFind, index + 1);
|
||||||
|
|
||||||
|
if (index == -1) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Terminate(this byte[] bytes) {
|
||||||
|
var terminated = new byte[bytes.Length + 1];
|
||||||
|
Array.Copy(bytes, terminated, bytes.Length);
|
||||||
|
terminated[terminated.Length - 1] = 0;
|
||||||
|
return terminated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe byte[] ReadTerminated(byte* mem) {
|
||||||
|
var bytes = new List<byte>();
|
||||||
|
while (*mem != 0) {
|
||||||
|
bytes.Add(*mem);
|
||||||
|
mem += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return bytes.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,25 +6,26 @@
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Version>1.5.1</Version>
|
<Version>1.5.1</Version>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="Dalamud, Version=5.2.4.2, Culture=neutral, PublicKeyToken=null">
|
<Reference Include="Dalamud">
|
||||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
|
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="ImGui.NET, Version=1.72.0.0, Culture=neutral, PublicKeyToken=null">
|
<Reference Include="ImGui.NET">
|
||||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
|
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="ImGuiScene, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
|
<Reference Include="ImGuiScene">
|
||||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
|
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Lumina, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null">
|
<Reference Include="Lumina">
|
||||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
|
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
<Reference Include="Lumina.Excel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
|
<Reference Include="Lumina.Excel">
|
||||||
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
|
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
@ -40,7 +41,8 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\XIVChatCommon\XIVChatCommon.csproj"/>
|
<ProjectReference Include="..\XIVChatCommon\XIVChatCommon.csproj"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<Target Name="CopyLibsodium" AfterTargets="AfterBuild">
|
<Target Name="CopyNativeLibraries" AfterTargets="AfterBuild">
|
||||||
<Copy SourceFiles="Resources\lib\libsodium.dll" DestinationFolder="$(OutDir)"/>
|
<Copy SourceFiles="Resources\lib\libsodium.dll" DestinationFolder="$(OutDir)"/>
|
||||||
|
<Copy SourceFiles="Resources\lib\xivchat_native_tools.dll" DestinationFolder="$(OutDir)"/>
|
||||||
</Target>
|
</Target>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
Loading…
Reference in New Issue