using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; namespace JXHelper.Core { public class ScannedSkill { public int SkillId { get; set; } public string Name { get; set; } = string.Empty; public int Level { get; set; } = 1; public int RequiredLevel { get; set; } = 1; public SkillType Type { get; set; } = SkillType.Attack; public bool IsBuff => Type == SkillType.ActiveBuff; public bool IsCurse => Type == SkillType.Curse; public bool IsPassive => Type == SkillType.Passive; public int CooldownSec { get; set; } = 60; public string Sect { get; set; } = string.Empty; public override string ToString() => Level > 1 ? $"{Name} ({Level})" : (RequiredLevel > 1 ? $"{Name} (cấp {RequiredLevel})" : Name); } public class CharacterSkillProfile { public string CharacterName { get; set; } = string.Empty; public string Sect { get; set; } = "Cái Bang"; public string RecommendedAttackSkill { get; set; } = string.Empty; public List AllSkills { get; set; } = new(); public List ActiveBuffSkills => AllSkills.Where(s => s.IsBuff).ToList(); public List CurseSkills => AllSkills.Where(s => s.IsCurse).ToList(); public List PassiveSkills => AllSkills.Where(s => s.IsPassive).ToList(); public List AttackSkills => AllSkills.Where(s => s.Type == SkillType.Attack).OrderBy(s => s.RequiredLevel).ToList(); public List BuffSkills => ActiveBuffSkills; } /// /// Động cơ tự động quét chiêu thức thực tế từ file cấu hình AutoVLBS và bộ nhớ game. /// public static class SkillScanner { private static readonly string[] PossibleVlbsPaths = new[] { @"C:\JXVCTK\JxVoCongTruyenKy\_AutoVLBS\VLBS19\UserData", @"C:\JXVCTK\JxVoCongTruyenKy\_AutoVLBS\VLBS\UserData", @"C:\AutoVLBS\UserData", }; /// /// Quét tất cả hồ sơ nhân vật và chiêu thức có sẵn. /// public static List ScanAllProfiles() { var profiles = new List(); foreach (var basePath in PossibleVlbsPaths) { if (!Directory.Exists(basePath)) continue; foreach (var cfgFile in Directory.GetFiles(basePath, "*.cfg")) { try { var charName = Path.GetFileNameWithoutExtension(cfgFile); var profile = ParseConfigFile(cfgFile, charName); if (profile != null && profile.AllSkills.Count > 0) { profiles.Add(profile); } } catch { // Bỏ qua file lỗi } } } return profiles; } /// /// Quét chiêu thức cho một nhân vật cụ thể, kết hợp môn phái tùy chọn hoặc tự động phát hiện. /// Tuyệt đối không gán nhầm chiêu của nhân vật khác. /// public static CharacterSkillProfile ScanCharacterSkills(string? targetCharName = null, string? preferredSect = null) { var all = ScanAllProfiles(); if (!string.IsNullOrEmpty(targetCharName)) { var match = all.FirstOrDefault(p => p.CharacterName.Equals(targetCharName, StringComparison.OrdinalIgnoreCase)); if (match != null && match.AllSkills.Count > 0) { // Nếu người dùng chọn môn phái khác với phái quét được, ưu tiên phái người dùng chọn if (!string.IsNullOrEmpty(preferredSect) && !match.Sect.Equals(preferredSect, StringComparison.OrdinalIgnoreCase)) { return BuildProfileForSect(targetCharName, preferredSect); } return match; } } // Nếu nhân vật không có trong UserData hoặc chưa quét chiêu, nạp bộ kỹ năng chuẩn theo môn phái string sect = !string.IsNullOrEmpty(preferredSect) ? preferredSect : "Cái Bang"; return BuildProfileForSect(targetCharName ?? "Nhân vật", sect); } /// /// Xây dựng hồ sơ kỹ năng chuẩn từ cơ sở dữ liệu cho một nhân vật và môn phái chỉ định. /// public static CharacterSkillProfile BuildProfileForSect(string charName, string sect) { var profile = new CharacterSkillProfile { CharacterName = charName, Sect = sect }; foreach (var def in SkillDatabase.GetSkillsBySect(sect)) { profile.AllSkills.Add(new ScannedSkill { SkillId = def.SkillId, Name = def.Name, Level = 1, RequiredLevel = def.RequiredLevel, Type = def.Type, CooldownSec = def.DefaultCooldownSec > 0 ? def.DefaultCooldownSec : 60, Sect = def.Sect }); } profile.RecommendedAttackSkill = GetDefaultAttackSkillForSect(sect); return profile; } /// /// Xác định chiêu thức công kích chính mặc định từ Bảng Kỹ Năng theo từng môn phái. /// Cái Bang mặc định là Kháng Long Hữu Hối (cấp 50). /// public static string GetDefaultAttackSkillForSect(string sect) { return sect switch { "Cái Bang" => "Kháng Long Hữu Hối", "Võ Đang" => "Kiếm Phi Kinh Thiên", "Nga My" => "Phật Quang Phổ Chiếu", "Ngũ Độc" => "Bách Độc Xuyên Tâm", "Thiếu Lâm" => "Kim Cang Phục Ma", "Thiên Vương" => "Đoạt Hồn Thích", "Đường Môn" => "Tiểu Lý Phi Đao", "Thúy Yên" => "Phong Hoa Tuyết Nguyệt", "Thiên Nhẫn" => "Ảo Ảnh Phi Hồ", "Côn Lôn" => "Cuồng Lôi Chấn Địa", _ => string.Empty }; } /// /// Phân tích file nhị phân .cfg của AutoVLBS (định dạng mảng struct 52 bytes tại offset 0x0fbc). /// public static CharacterSkillProfile? ParseConfigFile(string filePath, string charName) { if (!File.Exists(filePath)) return null; byte[] data; try { using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); data = new byte[fs.Length]; fs.Read(data, 0, data.Length); } catch { return null; } var profile = new CharacterSkillProfile { CharacterName = charName }; // Offset bắt đầu mảng chiêu thức trong AutoVLBS .cfg thường là 0x0fbc (4028) int startOffset = 0x0fbc; const int structSize = 52; // Nếu file đủ lớn để chứa bảng chiêu thức var rawSkills = new List(); if (data.Length >= startOffset + structSize) { // Quét các slot chiêu thức liên tục (dừng ngay khi gặp slot rỗng) for (int i = 0; i < 20; i++) { int offset = startOffset + i * structSize; if (offset + structSize > data.Length) break; int flag = BitConverter.ToInt32(data, offset); int skillId = BitConverter.ToInt32(data, offset + 4); int extraVal = BitConverter.ToInt32(data, offset + 8); // AutoVLBS lưu chiêu liên tục; gặp skillId <= 0 nghĩa là bảng chiêu thức nhân vật đã hết if (skillId <= 0) break; // Giải mã tên chiêu thức (TCVN3) string rawName = Tcvn3Encoding.Decode(data, offset + 12, 32); if (string.IsNullOrWhiteSpace(rawName)) break; // Tên chiêu thức hợp lệ phải bắt đầu bằng ký tự chữ và có độ dài tối thiểu if (rawName.Length < 2 || !char.IsLetter(rawName[0])) break; // Tách tên và level nếu có dạng "Tên (1)" int level = 1; var match = Regex.Match(rawName, @"^(.*?)\s*\((\d+)\)$"); string cleanName = rawName; if (match.Success) { cleanName = match.Groups[1].Value.Trim(); int.TryParse(match.Groups[2].Value, out level); } // Tra cứu thông tin từ SkillDatabase var def = SkillDatabase.FindByName(cleanName) ?? SkillDatabase.MasterSkills.FirstOrDefault(s => s.SkillId == skillId); SkillType skillType = def?.Type ?? (extraVal > 5000 ? SkillType.ActiveBuff : SkillType.Attack); int cooldown = def?.DefaultCooldownSec ?? (extraVal > 0 ? extraVal / 1000 : 60); string sect = def?.Sect ?? "Khác"; rawSkills.Add(new ScannedSkill { SkillId = skillId, Name = def?.Name ?? cleanName, Level = level, Type = skillType, CooldownSec = cooldown > 0 ? cooldown : 60, Sect = sect }); } } // Nếu không quét được từ offset cố định, tìm kiếm chuỗi pattern "(1)" trong toàn bộ file if (rawSkills.Count == 0) { for (int i = 0; i < data.Length - 40; i++) { if (data[i] == '(' && char.IsDigit((char)data[i + 1]) && data[i + 2] == ')') { int nameStart = Math.Max(0, i - 30); int zeroPos = -1; for (int j = i - 1; j >= nameStart; j--) { if (data[j] == 0) { zeroPos = j; break; } } if (zeroPos != -1) nameStart = zeroPos + 1; string raw = Tcvn3Encoding.Decode(data, nameStart, i + 3 - nameStart); if (!string.IsNullOrWhiteSpace(raw) && raw.Length > 3 && char.IsLetter(raw[0])) { var def = SkillDatabase.FindByName(raw); rawSkills.Add(new ScannedSkill { SkillId = def?.SkillId ?? 0, Name = def?.Name ?? raw, Level = data[i + 1] - '0', Type = def?.Type ?? SkillType.ActiveBuff, CooldownSec = def?.DefaultCooldownSec ?? 60, Sect = def?.Sect ?? "Khác" }); } } } } // 1. Xác định môn phái độc quyền của nhân vật dựa trên chiêu công kích và tỉ lệ chiêu hợp lệ var sectVotes = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var sk in rawSkills) { var def = SkillDatabase.FindByName(sk.Name) ?? SkillDatabase.MasterSkills.FirstOrDefault(s => s.SkillId == sk.SkillId); if (def != null && !string.IsNullOrEmpty(def.Sect) && def.Sect != "Khác") { // Chiêu công kích mang tính chất nhận diện môn phái chuẩn xác nhất (trọng số x3) int weight = def.IsBuff ? 1 : 3; sectVotes[def.Sect] = sectVotes.GetValueOrDefault(def.Sect, 0) + weight; } } if (sectVotes.Count > 0) { profile.Sect = sectVotes.OrderByDescending(kv => kv.Value).First().Key; } else { profile.Sect = "Cái Bang"; } // 2. Bộ lọc cách ly môn phái: Tuyệt đối LOẠI BỎ chiêu thức của môn phái khác (chiêu buff của đồng đội) var learnedBuffs = new List(); foreach (var sk in rawSkills) { var def = SkillDatabase.FindByName(sk.Name) ?? SkillDatabase.MasterSkills.FirstOrDefault(s => s.SkillId == sk.SkillId); if (def != null && !string.IsNullOrEmpty(def.Sect) && !def.Sect.Equals(profile.Sect, StringComparison.OrdinalIgnoreCase) && def.Sect != "Khác") { // Bỏ qua chiêu thức của đồng đội buff sang (ví dụ: Thiếu Lâm buff Dịch Cân Kinh cho Côn Lôn) continue; } sk.Sect = profile.Sect; if (def != null) { sk.Name = def.Name; sk.Type = def.Type; sk.RequiredLevel = def.RequiredLevel; if (def.DefaultCooldownSec > 0) sk.CooldownSec = def.DefaultCooldownSec; } learnedBuffs.Add(sk); } // 3. Trích xuất toàn diện Bảng Kỹ Năng (F5 Skill Table) của môn phái // Nạp toàn bộ chiêu thức chuẩn của phái (Attack, Buff, Passive, Curse), giữ nguyên cấp độ đã học từ file cfg profile.AllSkills.Clear(); foreach (var def in SkillDatabase.GetSkillsBySect(profile.Sect)) { var existing = learnedBuffs.FirstOrDefault(s => s.SkillId == def.SkillId || s.Name.Equals(def.Name, StringComparison.OrdinalIgnoreCase)); profile.AllSkills.Add(new ScannedSkill { SkillId = def.SkillId, Name = def.Name, Level = existing?.Level ?? 1, RequiredLevel = def.RequiredLevel, Type = def.Type, CooldownSec = existing?.CooldownSec ?? (def.DefaultCooldownSec > 0 ? def.DefaultCooldownSec : 60), Sect = def.Sect }); } // 4. Gán chiêu công kích đề xuất theo môn phái chuẩn xác (Cái Bang mặc định Kháng Long Hữu Hối cấp 50) profile.RecommendedAttackSkill = GetDefaultAttackSkillForSect(profile.Sect); return profile; } } }