using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Plugins;
public sealed record TranslationBundle(string GameSha256,IReadOnlyDictionary<string,string> Files)
{
public static TranslationBundle Reference { get; } = new("6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93",new Dictionary<string,string>
{
["dbghelp.dll"]="9e0c0ab1d854521918e59800496ff56d9bf97b436ad145f1ee88ffb33ea721fa",
["edchs.pkg"]="6d8256802b37041a9c819655f1cecd9432b6452171e794dba2d5a08313f1e898"
});
}
public sealed record TranslationStatus(bool Installed,bool Owned,string Message);
public sealed record TranslationInstallation(string Id,string GameExecutable,Dictionary<string,string> Files,DateTimeOffset InstalledAt);
/// <summary>Imports the user's local, verified translation bundle. Does not redistribute or decrypt it.</summary>
public sealed class ChineseTranslationManager(TranslationBundle? trusted=null)
{
private readonly TranslationBundle bundle=trusted??TranslationBundle.Reference;
private readonly SemaphoreSlim gate=new(1,1);
private const string Id="XFEEDToolBox.Chinese";
private static string Root(string game)
{
var path=Path.GetFullPath(game);
if(!Path.GetFileName(path).Equals("EliteDangerous64.exe",StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("请选择游戏主程序 EliteDangerous64.exe。");
return Path.GetDirectoryName(path)!;
}
private static string Owned(string game)
{
var path=Path.Combine(Root(game),".xfeedtoolbox-chinese");
if(Directory.Exists(path)&&(File.GetAttributes(path)&FileAttributes.ReparsePoint)!=0)throw new InvalidOperationException("汉化记录目录是链接,未操作。");
return path;
}
private static async Task<string> Hash(string file,CancellationToken token)
{await using var stream=new FileStream(file,FileMode.Open,FileAccess.Read,FileShare.Read);return Convert.ToHexStringLower(await SHA256.HashDataAsync(stream,token));}
public async Task<TranslationStatus> InspectAsync(string game,CancellationToken token=default)
{
var root=Root(game);var manifest=Path.Combine(Owned(game),"installation.json");var owned=File.Exists(manifest);
TranslationInstallation? record=null;
if(owned)
{
record=JsonSerializer.Deserialize<TranslationInstallation>(await File.ReadAllTextAsync(manifest,token),ToolboxSettings.JsonOptions);
if(record is null||record.Id!=Id||!record.GameExecutable.Equals(Path.GetFullPath(game),StringComparison.OrdinalIgnoreCase)||record.Files.Count!=bundle.Files.Count||record.Files.Any(p=>!bundle.Files.TryGetValue(p.Key,out var hash)||hash!=p.Value))throw new InvalidOperationException("汉化安装记录不匹配,未修改文件。");
}
foreach(var (name,hash) in bundle.Files)
{
var file=Path.Combine(root,name);
if(!File.Exists(file))return new(false,owned,owned?"汉化文件不完整,可还原本工具安装。":"未检测到完整汉化组件。");
if(await Hash(file,token)!=hash)return new(false,owned,"存在其他版本或被改动的汉化组件,已保留文件。");
}
return new(true,owned,owned?"简体中文汉化已安装。":"已有兼容的简体中文组件,无需重复安装。");
}
// Copy the complete verified bundle into the app's own resource store. Neither its source
// directory nor an external executable is needed for later installs from this store.
public async Task<string> ImportBundleAsync(string source,string resourceStore,CancellationToken token=default)
{
await gate.WaitAsync(token);
string? staging=null;
try
{
foreach(var(name,hash) in bundle.Files)
if(!File.Exists(Path.Combine(source,name))||await Hash(Path.Combine(source,name),token)!=hash)throw new InvalidOperationException("汉化资源缺失或校验不符:"+name);
var destination=Path.Combine(Path.GetFullPath(resourceStore),bundle.GameSha256);
if(Directory.Exists(destination))
{
foreach(var(name,hash) in bundle.Files)
if(!File.Exists(Path.Combine(destination,name))||await Hash(Path.Combine(destination,name),token)!=hash)throw new InvalidOperationException("本地汉化资源缓存不完整,请重新导入到新的资源目录。");
return destination;
}
Directory.CreateDirectory(Path.GetFullPath(resourceStore));staging=destination+"."+Guid.NewGuid().ToString("N")+".importing";Directory.CreateDirectory(staging);
foreach(var(name,hash) in bundle.Files)
{
var file=Path.Combine(staging,name);
await using(var input=File.OpenRead(Path.Combine(source,name)))await using(var output=new FileStream(file,FileMode.CreateNew,FileAccess.Write,FileShare.None))await input.CopyToAsync(output,token);
if(await Hash(file,token)!=hash)throw new IOException("复制期间汉化资源发生变化,导入已停止。");
}
Directory.Move(staging,destination);staging=null;return destination;
}
finally
{
try{if(staging is not null&&Directory.Exists(staging)){foreach(var file in Directory.EnumerateFiles(staging))File.Delete(file);Directory.Delete(staging,false);}}
finally{gate.Release();}
}
}
public async Task<TranslationStatus> InstallAsync(string game,string source,CancellationToken token=default)
{
await gate.WaitAsync(token);
try
{
var state=await InspectAsync(game,token);if(state.Installed)return state;
if(!File.Exists(game)||await Hash(game,token)!=bundle.GameSha256)throw new InvalidOperationException("该游戏构建尚未匹配汉化包,未安装。");
foreach(var (name,hash) in bundle.Files)
{
var file=Path.Combine(source,name);
if(!File.Exists(file)||await Hash(file,token)!=hash)throw new InvalidOperationException("本地汉化源缺失或校验不符:"+name);
if(File.Exists(Path.Combine(Root(game),name)))throw new InvalidOperationException("目标已有同名组件,保留原文件并停止安装:"+name);
}
var owned=Owned(game);Directory.CreateDirectory(owned);var moved=new List<string>();var staged=new List<string>();
if(bundle.Files.Keys.Any(name=>File.Exists(Path.Combine(owned,name+".installing"))))throw new InvalidOperationException("存在未完成的汉化暂存文件,已保留并停止。");
try
{
foreach(var (name,hash) in bundle.Files)
{
var staging=Path.Combine(owned,name+".installing");
await using(var input=File.OpenRead(Path.Combine(source,name)))await using(var output=new FileStream(staging,FileMode.CreateNew,FileAccess.Write,FileShare.None)){staged.Add(name);await input.CopyToAsync(output,token);}
if(await Hash(staging,token)!=hash)throw new IOException("复制期间源文件变化,安装已停止。");
}
foreach(var name in bundle.Files.Keys){token.ThrowIfCancellationRequested();File.Move(Path.Combine(owned,name+".installing"),Path.Combine(Root(game),name),false);moved.Add(name);}
var record=new TranslationInstallation(Id,Path.GetFullPath(game),new(bundle.Files),DateTimeOffset.UtcNow);
var manifest=Path.Combine(owned,"installation.json");await File.WriteAllTextAsync(manifest+".tmp",JsonSerializer.Serialize(record,ToolboxSettings.JsonOptions),token);File.Move(manifest+".tmp",manifest,true);
}
catch
{
foreach(var name in moved){var file=Path.Combine(Root(game),name);if(File.Exists(file)&&await Hash(file,CancellationToken.None)==bundle.Files[name])File.Delete(file);}
throw;
}
finally{foreach(var name in staged){var file=Path.Combine(owned,name+".installing");if(File.Exists(file))File.Delete(file);}}
return await InspectAsync(game,token);
}
finally{gate.Release();}
}
public async Task<TranslationStatus> UninstallAsync(string game,CancellationToken token=default)
{
await gate.WaitAsync(token);
try
{
var state=await InspectAsync(game,token);
if(!state.Owned)throw new InvalidOperationException("当前兼容组件不属于本工具的安装记录,已保留现有文件。");
foreach(var (name,hash) in bundle.Files){var file=Path.Combine(Root(game),name);if(File.Exists(file)&&await Hash(file,token)!=hash)throw new InvalidOperationException("组件已被改动,保留文件:"+name);}
// Test access for the entire bundle before removing either component.
var locks=new List<FileStream>();
try{foreach(var name in bundle.Files.Keys){var file=Path.Combine(Root(game),name);if(File.Exists(file))locks.Add(new(file,FileMode.Open,FileAccess.Read,FileShare.None));}}
finally{foreach(var held in locks)held.Dispose();}
foreach(var name in bundle.Files.Keys){var file=Path.Combine(Root(game),name);if(File.Exists(file))File.Delete(file);}
var owned=Owned(game);File.Delete(Path.Combine(owned,"installation.json"));if(!Directory.EnumerateFileSystemEntries(owned).Any())Directory.Delete(owned,false);
return new(false,false,"已还原本工具安装的简体中文组件;游戏 EXE 与贸易插件保持原样。");
}
finally{gate.Release();}
}
}
using System.Security.Cryptography;
using System.Text.Json;
using XFEEDToolBox.Core.Models;
namespace XFEEDToolBox.Core.Plugins;
public sealed record TranslationBundle(string GameSha256,IReadOnlyDictionary<string,string> Files)
{
public static TranslationBundle Reference { get; } = new("6891f70a021fa9f3a0e00458c284f48d1226e68ae7fc02ad48081aed47857e93",new Dictionary<string,string>
{
["dbghelp.dll"]="9e0c0ab1d854521918e59800496ff56d9bf97b436ad145f1ee88ffb33ea721fa",
["edchs.pkg"]="6d8256802b37041a9c819655f1cecd9432b6452171e794dba2d5a08313f1e898"
});
}
public sealed record TranslationStatus(bool Installed,bool Owned,string Message);
public sealed record TranslationInstallation(string Id,string GameExecutable,Dictionary<string,string> Files,DateTimeOffset InstalledAt);
/// <summary>Imports the user's local, verified translation bundle. Does not redistribute or decrypt it.</summary>
public sealed class ChineseTranslationManager(TranslationBundle? trusted=null)
{
private readonly TranslationBundle bundle=trusted??TranslationBundle.Reference;
private readonly SemaphoreSlim gate=new(1,1);
private const string Id="XFEEDToolBox.Chinese";
private static string Root(string game)
{
var path=Path.GetFullPath(game);
if(!Path.GetFileName(path).Equals("EliteDangerous64.exe",StringComparison.OrdinalIgnoreCase))throw new InvalidOperationException("请选择游戏主程序 EliteDangerous64.exe。");
return Path.GetDirectoryName(path)!;
}
private static string Owned(string game)
{
var path=Path.Combine(Root(game),".xfeedtoolbox-chinese");
if(Directory.Exists(path)&&(File.GetAttributes(path)&FileAttributes.ReparsePoint)!=0)throw new InvalidOperationException("汉化记录目录是链接,未操作。");
return path;
}
private static async Task<string> Hash(string file,CancellationToken token)
{await using var stream=new FileStream(file,FileMode.Open,FileAccess.Read,FileShare.Read);return Convert.ToHexStringLower(await SHA256.HashDataAsync(stream,token));}
public async Task<TranslationStatus> InspectAsync(string game,CancellationToken token=default)
{
var root=Root(game);var manifest=Path.Combine(Owned(game),"installation.json");var owned=File.Exists(manifest);
TranslationInstallation? record=null;
if(owned)
{
record=JsonSerializer.Deserialize<TranslationInstallation>(await File.ReadAllTextAsync(manifest,token),ToolboxSettings.JsonOptions);
if(record is null||record.Id!=Id||!record.GameExecutable.Equals(Path.GetFullPath(game),StringComparison.OrdinalIgnoreCase)||record.Files.Count!=bundle.Files.Count||record.Files.Any(p=>!bundle.Files.TryGetValue(p.Key,out var hash)||hash!=p.Value))throw new InvalidOperationException("汉化安装记录不匹配,未修改文件。");
}
foreach(var (name,hash) in bundle.Files)
{
var file=Path.Combine(root,name);
if(!File.Exists(file))return new(false,owned,owned?"汉化文件不完整,可还原本工具安装。":"未检测到完整汉化组件。");
if(await Hash(file,token)!=hash)return new(false,owned,"存在其他版本或被改动的汉化组件,已保留文件。");
}
return new(true,owned,owned?"简体中文汉化已安装。":"已有兼容的简体中文组件,无需重复安装。");
}
// Copy the complete verified bundle into the app's own resource store. Neither its source
// directory nor an external executable is needed for later installs from this store.
public async Task<string> ImportBundleAsync(string source,string resourceStore,CancellationToken token=default)
{
await gate.WaitAsync(token);
string? staging=null;
try
{
foreach(var(name,hash) in bundle.Files)
if(!File.Exists(Path.Combine(source,name))||await Hash(Path.Combine(source,name),token)!=hash)throw new InvalidOperationException("汉化资源缺失或校验不符:"+name);
var destination=Path.Combine(Path.GetFullPath(resourceStore),bundle.GameSha256);
if(Directory.Exists(destination))
{
foreach(var(name,hash) in bundle.Files)
if(!File.Exists(Path.Combine(destination,name))||await Hash(Path.Combine(destination,name),token)!=hash)throw new InvalidOperationException("本地汉化资源缓存不完整,请重新导入到新的资源目录。");
return destination;
}
Directory.CreateDirectory(Path.GetFullPath(resourceStore));staging=destination+"."+Guid.NewGuid().ToString("N")+".importing";Directory.CreateDirectory(staging);
foreach(var(name,hash) in bundle.Files)
{
var file=Path.Combine(staging,name);
await using(var input=File.OpenRead(Path.Combine(source,name)))await using(var output=new FileStream(file,FileMode.CreateNew,FileAccess.Write,FileShare.None))await input.CopyToAsync(output,token);
if(await Hash(file,token)!=hash)throw new IOException("复制期间汉化资源发生变化,导入已停止。");
}
Directory.Move(staging,destination);staging=null;return destination;
}
finally
{
try{if(staging is not null&&Directory.Exists(staging)){foreach(var file in Directory.EnumerateFiles(staging))File.Delete(file);Directory.Delete(staging,false);}}
finally{gate.Release();}
}
}
public async Task<TranslationStatus> InstallAsync(string game,string source,CancellationToken token=default)
{
await gate.WaitAsync(token);
try
{
var state=await InspectAsync(game,token);if(state.Installed)return state;
if(!File.Exists(game)||await Hash(game,token)!=bundle.GameSha256)throw new InvalidOperationException("该游戏构建尚未匹配汉化包,未安装。");
foreach(var (name,hash) in bundle.Files)
{
var file=Path.Combine(source,name);
if(!File.Exists(file)||await Hash(file,token)!=hash)throw new InvalidOperationException("本地汉化源缺失或校验不符:"+name);
if(File.Exists(Path.Combine(Root(game),name)))throw new InvalidOperationException("目标已有同名组件,保留原文件并停止安装:"+name);
}
var owned=Owned(game);Directory.CreateDirectory(owned);var moved=new List<string>();var staged=new List<string>();
if(bundle.Files.Keys.Any(name=>File.Exists(Path.Combine(owned,name+".installing"))))throw new InvalidOperationException("存在未完成的汉化暂存文件,已保留并停止。");
try
{
foreach(var (name,hash) in bundle.Files)
{
var staging=Path.Combine(owned,name+".installing");
await using(var input=File.OpenRead(Path.Combine(source,name)))await using(var output=new FileStream(staging,FileMode.CreateNew,FileAccess.Write,FileShare.None)){staged.Add(name);await input.CopyToAsync(output,token);}
if(await Hash(staging,token)!=hash)throw new IOException("复制期间源文件变化,安装已停止。");
}
foreach(var name in bundle.Files.Keys){token.ThrowIfCancellationRequested();File.Move(Path.Combine(owned,name+".installing"),Path.Combine(Root(game),name),false);moved.Add(name);}
var record=new TranslationInstallation(Id,Path.GetFullPath(game),new(bundle.Files),DateTimeOffset.UtcNow);
var manifest=Path.Combine(owned,"installation.json");await File.WriteAllTextAsync(manifest+".tmp",JsonSerializer.Serialize(record,ToolboxSettings.JsonOptions),token);File.Move(manifest+".tmp",manifest,true);
}
catch
{
foreach(var name in moved){var file=Path.Combine(Root(game),name);if(File.Exists(file)&&await Hash(file,CancellationToken.None)==bundle.Files[name])File.Delete(file);}
throw;
}
finally{foreach(var name in staged){var file=Path.Combine(owned,name+".installing");if(File.Exists(file))File.Delete(file);}}
return await InspectAsync(game,token);
}
finally{gate.Release();}
}
public async Task<TranslationStatus> UninstallAsync(string game,CancellationToken token=default)
{
await gate.WaitAsync(token);
try
{
var state=await InspectAsync(game,token);
if(!state.Owned)throw new InvalidOperationException("当前兼容组件不属于本工具的安装记录,已保留现有文件。");
foreach(var (name,hash) in bundle.Files){var file=Path.Combine(Root(game),name);if(File.Exists(file)&&await Hash(file,token)!=hash)throw new InvalidOperationException("组件已被改动,保留文件:"+name);}
// Test access for the entire bundle before removing either component.
var locks=new List<FileStream>();
try{foreach(var name in bundle.Files.Keys){var file=Path.Combine(Root(game),name);if(File.Exists(file))locks.Add(new(file,FileMode.Open,FileAccess.Read,FileShare.None));}}
finally{foreach(var held in locks)held.Dispose();}
foreach(var name in bundle.Files.Keys){var file=Path.Combine(Root(game),name);if(File.Exists(file))File.Delete(file);}
var owned=Owned(game);File.Delete(Path.Combine(owned,"installation.json"));if(!Directory.EnumerateFileSystemEntries(owned).Any())Directory.Delete(owned,false);
return new(false,false,"已还原本工具安装的简体中文组件;游戏 EXE 与贸易插件保持原样。");
}
finally{gate.Release();}
}
}