using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; namespace JXHelper.Core; /// /// Động cơ đối soát và bắt chính xác tùy chọn đối thoại NPC theo nội dung text (Text-Based Option Matcher). /// Khắc phục hoàn toàn nhược điểm của việc chọn cố định theo thứ tự (Option 1 / Phím 1) khi server đổi menu hoặc chèn sự kiện. /// Hỗ trợ cả chuỗi mã hóa TCVN3 (ABC), Unicode tiếng Việt chuẩn và văn bản không dấu. /// public static class NpcDialogTextMatcher { // Từ khóa mặc định chuẩn cho các tác vụ phổ biến trong Võ Lâm 1 public const string DefaultRepairKeywords = "Sửa trang bị;Sửa chữa;Sửa đồ;Sửa vũ khí;Söa trang bÞ;Söa ch÷a;Söa"; public const string DefaultRepairSubMenuKeywords = "Sửa toàn bộ;Sửa tất cả;Toàn bộ trang bị;Toàn bộ;Toµn bé;TÊt c¶"; public const string DefaultMedicineKeywords = "Dược phẩm tân thủ;Thuốc tân thủ;Nhận hỗ trợ;Dược phẩm;D­îc phÈm;Tn thñ"; public const string DefaultTravelKeywords = "Những nơi đã đi qua;Khu vực luyện công;Điểm luyện công;Nh÷ng n¬i"; /// /// Chuẩn hóa chuỗi để so khớp: chuyển về chữ thường, giải mã TCVN3 nếu có, và loại bỏ dấu thanh. /// public static string NormalizeForMatching(string input) { if (string.IsNullOrWhiteSpace(input)) return string.Empty; // 1. Chuyển đổi TCVN3 sang Unicode nếu phát hiện ký tự đặc trưng string unicode = Tcvn3Encoding.ToUnicode(input); // 2. Chuyển về chữ thường string lower = unicode.ToLowerInvariant().Trim(); // 3. Loại bỏ ký tự số thứ tự đầu dòng (ví dụ "1. ", "2. ", "[1] ", "(1) ") lower = Regex.Replace(lower, @"^[\(\[]?\d+[\.\:\-\)\]]\s*", ""); // 4. Thu gọn nhiều khoảng trắng liên tiếp lower = Regex.Replace(lower, @"\s+", " ").Trim(); return lower; } /// /// Loại bỏ toàn bộ dấu thanh tiếng Việt (phục vụ đối soát không dấu). /// public static string RemoveDiacritics(string text) { if (string.IsNullOrWhiteSpace(text)) return string.Empty; string normalizedString = text.Normalize(NormalizationForm.FormD); var stringBuilder = new StringBuilder(); foreach (char c in normalizedString) { var unicodeCategory = System.Globalization.CharUnicodeInfo.GetUnicodeCategory(c); if (unicodeCategory != System.Globalization.UnicodeCategory.NonSpacingMark) { stringBuilder.Append(c); } } return stringBuilder.ToString().Normalize(NormalizationForm.FormC).Replace('đ', 'd').Replace('Đ', 'D'); } /// /// Kiểm tra xem một dòng tùy chọn có khớp với bất kỳ từ khóa nào trong chuỗi phân tách không. /// public static bool MatchesKeywords(string optionText, string keywordString) { if (string.IsNullOrWhiteSpace(keywordString)) return false; var keywords = keywordString .Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(k => k.Trim()) .Where(k => !string.IsNullOrEmpty(k)); return MatchesKeywords(optionText, keywords); } /// /// Kiểm tra xem một dòng tùy chọn có khớp với bất kỳ từ khóa nào trong danh sách không. /// public static bool MatchesKeywords(string optionText, IEnumerable keywords) { if (string.IsNullOrWhiteSpace(optionText)) return false; string normOption = NormalizeForMatching(optionText); string plainOption = RemoveDiacritics(normOption); foreach (var kw in keywords) { if (string.IsNullOrWhiteSpace(kw)) continue; string normKw = NormalizeForMatching(kw); string plainKw = RemoveDiacritics(normKw); // 1. So khớp Unicode gốc (sau chuẩn hóa TCVN3) if (normOption.Contains(normKw, StringComparison.OrdinalIgnoreCase)) { return true; } // 2. So khớp không dấu nếu từ khóa dài >= 3 ký tự if (plainKw.Length >= 3 && plainOption.Contains(plainKw, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } /// /// Tìm vị trí dòng (1-based index) phù hợp nhất từ danh sách các tùy chọn có sẵn. /// /// Danh sách các dòng tùy chọn hiện có trong bảng thoại NPC /// Chuỗi từ khóa phân tách bằng dấu chấm phẩy hoặc dấu phẩy /// Chỉ số dự phòng nếu không tìm thấy dòng nào khớp (mặc định 1) /// Chuỗi text thực tế đã khớp (nếu tìm thấy) /// Chỉ số 1-based (1, 2, 3...) của tùy chọn cần chọn public static int ResolveOptionIndex( IReadOnlyList options, string keywordString, int fallbackIndex, out string? matchedText) { matchedText = null; if (options == null || options.Count == 0) { return Math.Max(1, fallbackIndex); } var keywords = (keywordString ?? string.Empty) .Split(new[] { ';', ',' }, StringSplitOptions.RemoveEmptyEntries) .Select(k => k.Trim()) .Where(k => !string.IsNullOrEmpty(k)) .ToList(); if (keywords.Count == 0) { return Math.Max(1, fallbackIndex); } for (int i = 0; i < options.Count; i++) { string line = options[i]; if (MatchesKeywords(line, keywords)) { matchedText = line; return i + 1; // 1-based index } } return Math.Max(1, fallbackIndex); } /// /// Tìm vị trí dòng (1-based index) phù hợp nhất từ danh sách các tùy chọn có sẵn (không cần out param). /// public static int ResolveOptionIndex( IReadOnlyList options, string keywordString, int fallbackIndex = 1) { return ResolveOptionIndex(options, keywordString, fallbackIndex, out _); } /// /// Tìm vị trí dòng (1-based index) với danh sách IEnumerable từ khóa. /// public static int ResolveOptionIndex( IReadOnlyList options, IEnumerable keywords, int fallbackIndex = 1) { string kwStr = string.Join(";", keywords); return ResolveOptionIndex(options, kwStr, fallbackIndex, out _); } /// /// Tính toán mã Virtual Key (0x31 = '1', 0x32 = '2', ...) tương ứng với chỉ số 1-based. /// public static int GetVirtualKeyForIndex(int oneBasedIndex) { int clamped = Math.Clamp(oneBasedIndex, 1, 9); return 0x30 + clamped; // 0x31 = '1', 0x32 = '2'... } /// /// Tính toán ký tự char ('1', '2', ...) tương ứng với chỉ số 1-based. /// public static char GetCharForIndex(int oneBasedIndex) { int clamped = Math.Clamp(oneBasedIndex, 1, 9); return (char)('0' + clamped); } }