using System.Runtime.InteropServices; using System.Text; using Dalamud; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Plugin.Services; using Dalamud.Utility; using Lumina.Excel; using Lumina.Excel.GeneratedSheets; using Pidgin; using static Pidgin.Parser; using static Pidgin.Parser; using TextPayload = Lumina.Text.Payloads.TextPayload; namespace ChatTwo.Util; internal static class AutoTranslate { private static readonly Dictionary> Entries = new(); private static readonly HashSet<(uint, uint)> ValidEntries = new(); private static Parser> selector)> Parser() { var sheetName = Any .AtLeastOnceUntil(Lookahead(Char('[').IgnoreResult().Or(End))) .Select(string.Concat) .Labelled("sheetName"); var numPair = Map( (first, second) => (ISelectorPart) new IndexRange( uint.Parse(string.Concat(first)), uint.Parse(string.Concat(second)) ), Digit.AtLeastOnce().Before(Char('-')), Digit.AtLeastOnce() ) .Labelled("numPair"); var singleRow = Digit .AtLeastOnce() .Select(string.Concat) .Select(num => (ISelectorPart) new SingleRow(uint.Parse(num))); var column = String("col-") .Then(Digit.AtLeastOnce()) .Select(string.Concat) .Select(num => (ISelectorPart) new ColumnSpecifier(uint.Parse(num))); var noun = String("noun") .Select(_ => (ISelectorPart) new NounMarker()); var selectorItems = OneOf( Try(numPair), singleRow, column, noun ) .Separated(Char(',')) .Labelled("selectorItems"); var selector = selectorItems .Between(Char('['), Char(']')) .Labelled("selector"); return Map( (name, selector) => (name, selector), sheetName, selector.Optional() ); } private static string TextValue(this Lumina.Text.SeString str) { var payloads = str.Payloads .Select(p => { if (p is TextPayload text) { return p.Data[0] == 0x03 ? text.RawString[1..] : text.RawString; } if (p.Data.Length <= 1) { return ""; } if (p.Data[1] == 0x1F) { return "-"; } if (p.Data.Length > 2 && p.Data[1] == 0x20) { var value = p.Data.Length > 4 ? p.Data[3] - 1 : p.Data[2]; return ((char) (48 + value)).ToString(); } return ""; }); return string.Join("", payloads); } private static List AllEntries(IDataManager data) { if (Entries.TryGetValue(data.Language, out var entries)) { return entries; } var shouldAdd = ValidEntries.Count == 0; var parser = Parser(); var list = new List(); foreach (var row in data.GetExcelSheet()!) { var lookup = row.LookupTable.TextValue(); if (lookup is not ("" or "@")) { var (sheetName, selector) = parser.ParseOrThrow(lookup); var sheetType = typeof(Completion) .Assembly .GetType($"Lumina.Excel.GeneratedSheets.{sheetName}")!; var getSheet = data .GetType() .GetMethod("GetExcelSheet", Type.EmptyTypes)! .MakeGenericMethod(sheetType); var sheet = (ExcelSheetImpl) getSheet.Invoke(data, null)!; var rowParsers = sheet.GetRowParsers().ToArray(); var columns = new List(); var rows = new List(); if (selector.HasValue) { columns.Clear(); rows.Clear(); foreach (var part in selector.Value) { switch (part) { case IndexRange range: { var start = (int) range.Start; var end = (int) (range.End + 1); rows.Add(start..end); break; } case SingleRow single: { var idx = (int) single.Row; rows.Add(idx..(idx + 1)); break; } case ColumnSpecifier col: columns.Add((int) col.Column); break; } } } if (columns.Count == 0) { columns.Add(0); } if (rows.Count == 0) { rows.Add(..); } var validRows = rowParsers .Select(parser => parser.RowId) .ToArray(); foreach (var range in rows) { for (var i = range.Start.Value; i < range.End.Value; i++) { if (!validRows.Contains((uint) i)) { continue; } foreach (var col in columns) { var rowParser = rowParsers.FirstOrDefault(parser => parser.RowId == i); if (rowParser == null) { continue; } var rawName = rowParser.ReadColumn(col)!; var name = rawName.ToDalamudString(); var text = name.TextValue; if (text.Length > 0) { list.Add(new AutoTranslateEntry( row.Group, (uint) i, text, name )); if (shouldAdd) { ValidEntries.Add((row.Group, (uint) i)); } } } } } } else if (lookup is not "@") { var text = row.Text.ToDalamudString(); list.Add(new AutoTranslateEntry( row.Group, row.RowId, text.TextValue, text )); if (shouldAdd) { ValidEntries.Add((row.Group, row.RowId)); } } } Entries[data.Language] = list; return list; } internal static List Matching(IDataManager data, string prefix, bool sort) { var wholeMatches = new List(); var prefixMatches = new List(); var otherMatches = new List(); foreach (var entry in AllEntries(data)) { if (entry.String.Equals(prefix, StringComparison.OrdinalIgnoreCase)) { wholeMatches.Add(entry); } else if (entry.String.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) { prefixMatches.Add(entry); } else if (entry.String.Contains(prefix, StringComparison.OrdinalIgnoreCase)) { otherMatches.Add(entry); } } if (sort) { return wholeMatches.OrderBy(entry => entry.String, StringComparer.OrdinalIgnoreCase) .Concat(prefixMatches.OrderBy(entry => entry.String, StringComparer.OrdinalIgnoreCase)) .Concat(otherMatches.OrderBy(entry => entry.String, StringComparer.OrdinalIgnoreCase)) .ToList(); } return wholeMatches .Concat(prefixMatches) .Concat(otherMatches) .ToList(); } [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)] private static extern int memcmp(byte[] b1, byte[] b2, UIntPtr count); internal static void ReplaceWithPayload(IDataManager data, ref byte[] bytes) { var search = Encoding.UTF8.GetBytes("') { var tag = Encoding.UTF8.GetString(bytes[start..(i + 1)]); var parts = tag[4..^1].Split(',', 2); if (parts.Length == 2 && uint.TryParse(parts[0], out var group) && uint.TryParse(parts[1], out var key)) { var payload = ValidEntries.Contains((group, key)) ? new AutoTranslatePayload(group, key).Encode() : Array.Empty(); var oldBytes = bytes.ToArray(); var lengthDiff = payload.Length - (i - start); bytes = new byte[oldBytes.Length + lengthDiff]; Array.Copy(oldBytes, bytes, start); Array.Copy(payload, 0, bytes, start, payload.Length); Array.Copy(oldBytes, i + 1, bytes, start + payload.Length, oldBytes.Length - (i + 1)); i += lengthDiff; } start = -1; } else { continue; } } if (i + search.Length < bytes.Length && memcmp(bytes[i..], search, (UIntPtr) search.Length) == 0) { start = i; } } } } internal interface ISelectorPart { } internal class SingleRow : ISelectorPart { public uint Row { get; } public SingleRow(uint row) { this.Row = row; } } internal class IndexRange : ISelectorPart { public uint Start { get; } public uint End { get; } public IndexRange(uint start, uint end) { this.Start = start; this.End = end; } } internal class NounMarker : ISelectorPart { } internal class ColumnSpecifier : ISelectorPart { public uint Column { get; } public ColumnSpecifier(uint column) { this.Column = column; } } internal class AutoTranslateEntry { internal uint Group { get; } internal uint Row { get; } internal string String { get; } internal SeString SeString { get; } public AutoTranslateEntry(uint group, uint row, string str, SeString seStr) { this.Group = group; this.Row = row; this.String = str; this.SeString = seStr; } }