Files
CapySR/gui/LanguagePatch.cs
T
2026-10-17 21:03:23 -07:00

198 lines
7.8 KiB
C#

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<string> Codes = new List<string>();
}
public static class LanguagePatch
{
static readonly Dictionary<string, string> KnownNames = new Dictionary<string, string>
{
{ "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<int> Entries = new List<int>();
public int Default;
}
class Hit
{
public string File;
public string Folder;
public Row Row;
public List<string> 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<Hit> Find(string client)
{
var hits = new List<Hit>();
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;
}
}
}