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:
Anna 2021-06-06 20:45:39 -04:00
parent e41b90d63e
commit c092bf2019
7 changed files with 137 additions and 60 deletions

View File

@ -6,6 +6,7 @@
<InputAssemblies Include="$(OutputPath)\*.dll"
Exclude="$(OutputPath)\$(AssemblyName).dll;
$(OutputPath)\libsodium.dll;
$(OutputPath)\xivchat_native_tools.dll;
$(OutputPath)\System.Buffers.dll;
$(OutputPath)\System.Memory.dll;
$(OutputPath)\System.Numerics.Vectors.dll"/>

View File

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

View File

@ -6,6 +6,7 @@ using Sodium;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Sockets;
@ -23,8 +24,25 @@ using XIVChatCommon.Message.Server;
namespace XIVChatPlugin {
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 Stopwatch _sendWatch = new();
private readonly CancellationTokenSource _tokenSource = new();
private readonly ConcurrentQueue<string> _toGame = new();
@ -50,11 +68,13 @@ namespace XIVChatPlugin {
private const int MaxMessageSize = 128_000;
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) {
this.RegenerateKeyPair();
}
this._sendWatch.Start();
this._plugin.Functions.ReceiveFriendList += this.OnReceiveFriendList;
}
@ -194,7 +214,7 @@ namespace XIVChatPlugin {
if (sender.Payloads.Count > 0) {
var format = this.FormatFor(chatCode.Type);
if (format != null && format.IsPresent) {
if (format is { IsPresent: true }) {
chunks.Add(new TextChunk(format.Before) {
FallbackColour = colour,
});
@ -251,10 +271,25 @@ namespace XIVChatPlugin {
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)) {
return;
}
this._sendWatch.Restart();
this._plugin.Functions.ProcessChatBox(message);
}
@ -575,7 +610,7 @@ namespace XIVChatPlugin {
chunks.Add(new TextChunk(text) {
FallbackColour = defaultColour,
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,
});
}
@ -643,9 +678,7 @@ namespace XIVChatPlugin {
private IEnumerable<ServerMessage> MessagesAfter(DateTime time) => this._backlog.Where(msg => msg.Timestamp > time).ToArray();
private static IEnumerable<string> Wrap(string input) {
const int limit = 500;
if (input.Length <= limit) {
if (input.Length <= MaxMessageLength) {
return new[] {
input,
};
@ -656,59 +689,22 @@ namespace XIVChatPlugin {
var space = input.IndexOf(' ');
if (space != -1) {
prefix = input.Substring(0, space);
input = input.Substring(space + 1);
}
}
var parts = new List<string>();
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;
// handle wrapping tells
if (prefix is "/tell" or "/t") {
var tellSpace = input.IndexOfCount(' ', 3);
if (tellSpace != -1) {
prefix = input.Substring(0, tellSpace);
input = input.Substring(tellSpace + 1);
}
var part = word.Length - start < partLength ? word.Substring(start) : word.Substring(start, partLength);
if (part.Length == 0) {
continue;
}
if (prefix.Length != 0) {
part = prefix + " " + part;
}
parts.Add(part);
} else {
input = input.Substring(space + 1);
}
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) {
parts.Add(builder.ToString().TrimEnd(' '));
}
return parts.ToArray();
return NativeTools.Wrap(input, MaxMessageLength)
.Select(text => $"{prefix} {text}")
.ToArray();
}
private void BroadcastMessage(Encodable message) {

36
XIVChatPlugin/Util.cs Normal file
View File

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

View File

@ -6,25 +6,26 @@
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<Version>1.5.1</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<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>
<Private>False</Private>
</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>
<Private>False</Private>
</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>
<Private>False</Private>
</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>
<Private>False</Private>
</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>
<Private>False</Private>
</Reference>
@ -40,7 +41,8 @@
<ItemGroup>
<ProjectReference Include="..\XIVChatCommon\XIVChatCommon.csproj"/>
</ItemGroup>
<Target Name="CopyLibsodium" AfterTargets="AfterBuild">
<Target Name="CopyNativeLibraries" AfterTargets="AfterBuild">
<Copy SourceFiles="Resources\lib\libsodium.dll" DestinationFolder="$(OutDir)"/>
<Copy SourceFiles="Resources\lib\xivchat_native_tools.dll" DestinationFolder="$(OutDir)"/>
</Target>
</Project>