XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEEDToolBox

【WinUI】XFE精英危险工具箱

公开
关注 0 Fork 0 Star 0
UTF-8
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();}
    }
}