using System.Diagnostics; using System.Globalization; using System.Runtime.InteropServices; using System.Text; using Dalamud.Game; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Utility; using Lumina.Excel; using Lumina.Text.Payloads; using Lumina.Text.ReadOnly; using Pidgin; using static Pidgin.Parser; using static Pidgin.Parser; namespace ChatTwo.Util; internal static class AutoTranslate { private static readonly Dictionary> Entries = new(); private static readonly HashSet<(uint, uint)> ValidEntries = []; private static Parser> selector)> Parser() { var sheetName = Any .AtLeastOnceUntil(Lookahead(Char('[').IgnoreResult().Or(End))) .Select(string.Concat) .Labelled("sheetName"); var numPair = Map(ISelectorPart (first, second) => 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(ISelectorPart (num) => new SingleRow(uint.Parse(num))); var column = String("col-") .Then(Digit.AtLeastOnce()) .Select(string.Concat) .Select(ISelectorPart (num) => 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, sel) => (name, sel), sheetName, selector.Optional()); } /// /// Preloads auto-translate entries into the cache for the current game /// language. Without this, the first message will take a long time to send /// (which causes a hitch in the main thread). /// /// This spawns a new thread. /// internal static void PreloadCache() { new Thread(() => { var sw = Stopwatch.StartNew(); AllEntries(); Plugin.Log.Debug($"Warming up auto-translate took {sw.ElapsedMilliseconds}ms"); }).Start(); } private static List AllEntries() { if (Entries.TryGetValue(Plugin.DataManager.Language, out var entries)) return entries; var shouldAdd = ValidEntries.Count == 0; var parser = Parser(); var list = new List(); foreach (var row in Sheets.CompletionSheet) { var lookup = string.Concat(row.LookupTable.Select(p => p.Type == ReadOnlySePayloadType.Text ? Encoding.UTF8.GetString(p.Body.Span) : p.MacroCode == MacroCode.Num && p.TryGetExpression(out var num) && num.TryGetInt(out var val) ? val.ToString(CultureInfo.InvariantCulture) : ",,,unexpected macro code,,,")); try { if (lookup is not ("" or "@")) { // SE added whitespace to the newest additions, but ParseOrThrow doesn't see them as valid lookup = lookup.Replace(" ", ""); var (sheetName, selector) = parser.ParseOrThrow(lookup); var sheet = Plugin.DataManager.Excel.GetSheet(name: sheetName); 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) // We can't use an "index from end" (like `^0`) here because // we're iterating over integers, not an array directly. // Previously, we were setting `0..^0` which caused these // sheets to be completely skipped due to this bug. // See below. rows.Add(..Index.FromStart((int)sheet.GetRowAt(sheet.Count - 1).RowId + 1)); foreach (var range in rows) { // We iterate over the range by numerical values here, so // we can't use an "index from end" otherwise nothing will // happen. // See above. for (var i = range.Start.Value; i < range.End.Value; i++) { if (!sheet.TryGetRow((uint)i, out var rowParser)) continue; foreach (var col in columns) { var rawName = rowParser.ReadStringColumn(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)); } } catch (Exception ex) { Plugin.Log.Error(ex, $"failed to translate: {lookup}"); } } Entries[Plugin.DataManager.Language] = list; return list; } internal static List Matching(string prefix, bool sort) { var wholeMatches = new List(); var prefixMatches = new List(); var otherMatches = new List(); foreach (var entry in AllEntries()) { 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, nuint count); internal static void ReplaceWithPayload(ref byte[] bytes) { var search = "