198 lines
7.8 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|