using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; namespace CapyLauncher { public class LanguageState { public string Current; public List Codes = new List(); } public static class LanguagePatch { static readonly Dictionary KnownNames = new Dictionary { { "en", "English" }, { "cn", "中文" }, { "jp", "日本語" }, { "kr", "한국어" }, { "fr", "Français" }, { "de", "Deutsch" }, { "es", "Español" }, { "pt", "Português" }, { "ru", "Русский" }, { "th", "ไทย" }, { "vi", "Tiếng Việt" }, { "id", "Indonesia" }, }; static readonly string[] Folders = { "StreamingAssets", "Persistent" }; static readonly byte[] CnArea = { 0x02, (byte)'c', (byte)'n' }; public static string Name(string code) => KnownNames.TryGetValue(code, out string name) ? name : code.ToUpperInvariant(); class Row { public int Start; public int End; public string Area; public List Entries = new List(); public int Default; } class Hit { public string File; public string Folder; public Row Row; public List Offered; public string Code; } static bool Code(byte[] data, int at) => at + 3 <= data.Length && data[at] == 0x02 && data[at + 1] >= 'a' && data[at + 1] <= 'z' && data[at + 2] >= 'a' && data[at + 2] <= 'z'; static string Text(byte[] data, int at) => Encoding.ASCII.GetString(data, at + 1, 2); static bool Varint(byte[] data, ref int at, out ulong value) { value = 0; for (int shift = 0; shift < 35; shift += 7) { if (at >= data.Length) return false; byte b = data[at++]; value |= (ulong)(b & 0x7F) << shift; if ((b & 0x80) == 0) return true; } return false; } static Row ReadRow(byte[] data, int start) { if (start < 0 || start >= data.Length) return null; byte flags = data[start]; if ((flags & ~0x0F) != 0 || (flags & 0x0D) != 0x0D) return null; int at = start + 1; if (!Code(data, at)) return null; var row = new Row { Start = start, Area = Text(data, at) }; at += 3; if ((flags & 0x02) != 0 && !Varint(data, ref at, out _)) return null; if (!Varint(data, ref at, out ulong raw)) return null; long count = (long)(raw >> 1) ^ -(long)(raw & 1); if (count < 1 || count > 24) return null; for (int i = 0; i < count; i++) { if (!Code(data, at)) return null; row.Entries.Add(at); at += 3; } if (!Code(data, at)) return null; row.Default = at; row.End = at + 3; return row; } static Row Neighbour(byte[] data, Row cn) { for (int start = cn.Start - 1; start >= Math.Max(0, cn.Start - 160); start--) { Row before = ReadRow(data, start); if (before != null && before.End == cn.Start) return before.Area == "os" ? before : null; } Row after = ReadRow(data, cn.End); return after != null && after.Area == "os" ? after : null; } static IEnumerable<(string folder, string file)> Bundles(string client) { foreach (string folder in Folders) { string dir = Path.Combine(client, "StarRail_Data", folder, "DesignData", "Windows"); if (!Directory.Exists(dir)) continue; foreach (string file in Directory.GetFiles(dir, "*.bytes", SearchOption.AllDirectories)) { string name = Path.GetFileName(file); if (!name.StartsWith("DesignV", StringComparison.OrdinalIgnoreCase) && !name.StartsWith("M_", StringComparison.OrdinalIgnoreCase)) yield return (folder, file); } } } static List Find(string client) { var hits = new List(); foreach (var (folder, file) in Bundles(client)) { byte[] data = File.ReadAllBytes(file); for (int i = IndexOf(data, CnArea, 1); i >= 0; i = IndexOf(data, CnArea, i + 1)) { Row cn = ReadRow(data, i - 1); if (cn == null) continue; Row os = Neighbour(data, cn); if (os == null) continue; var offered = os.Entries.Select(e => Text(data, e)).Distinct().ToList(); if (!offered.Contains("cn")) continue; if (!offered.Contains("en")) offered.Insert(0, "en"); hits.Add(new Hit { File = file, Folder = folder, Row = cn, Offered = offered, Code = Text(data, cn.Default) }); } } return hits; } static int IndexOf(byte[] data, byte[] needle, int from) { for (int i = Array.IndexOf(data, needle[0], from); i >= 0 && i + needle.Length <= data.Length; i = Array.IndexOf(data, needle[0], i + 1)) { int n = 1; while (n < needle.Length && data[i + n] == needle[n]) n++; if (n == needle.Length) return i; } return -1; } public static LanguageState Detect(string client) { if (string.IsNullOrEmpty(client) || !Directory.Exists(client)) return null; var hits = Find(client); if (hits.Count == 0) return null; var codes = hits[0].Offered.ToList(); if (!codes.Contains(hits[0].Code)) codes.Add(hits[0].Code); codes = codes.OrderBy(c => c == "en" ? 0 : 1).ToList(); return new LanguageState { Current = hits[0].Code, Codes = codes }; } public static int Apply(string client, string code) { if (code == null || code.Length != 2 || !code.All(c => c >= 'a' && c <= 'z')) throw new ArgumentException("unknown language " + code); byte[] value = Encoding.ASCII.GetBytes(code); int patched = 0; foreach (var group in Find(client).GroupBy(h => h.File)) { var hits = group.ToList(); byte[] before = File.ReadAllBytes(group.Key); string backup = Path.Combine(client, "_lang_backup", group.First().Folder, Path.GetFileName(group.Key) + ".cn.bak"); bool changed = false; using (var stream = new FileStream(group.Key, FileMode.Open, FileAccess.Write, FileShare.Read)) { foreach (Hit hit in hits) { int first = hit.Row.Entries[0]; if (Text(before, first) == code && hit.Code == code) continue; if (!changed && !File.Exists(backup)) { Directory.CreateDirectory(Path.GetDirectoryName(backup)); File.WriteAllBytes(backup, before); } stream.Position = first + 1; stream.Write(value, 0, 2); stream.Position = hit.Row.Default + 1; stream.Write(value, 0, 2); changed = true; } } if (changed) patched++; } return patched; } } }