From 2518ae0b9032fbd67a482c9b1826be8e4642dc44 Mon Sep 17 00:00:00 2001 From: DismissedLight <1686188646@qq.com> Date: Fri, 27 Jan 2023 11:22:25 +0800 Subject: [PATCH] package convert impl --- .../Snap.Hutao.Installer/Program.cs | 2 +- src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 1 - .../DependencyInjection/IocConfiguration.cs | 2 + .../Snap.Hutao/Core/IO/FileDigest.cs | 31 ++ src/Snap.Hutao/Snap.Hutao/Core/IO/FilePath.cs | 2 +- .../Snap.Hutao/Core/Setting/SettingKeys.cs | 10 +- .../Snap.Hutao/Core/Setting/StaticResource.cs | 14 +- .../Model/Binding/LaunchGame/LaunchScheme.cs | 35 +- .../Snap.Hutao/Model/Metadata/AvatarIds.cs | 2 + .../Snap.Hutao/Package.appxmanifest | 4 +- .../Factory/GachaStatisticsExtensions.cs | 7 +- .../Factory/GachaStatisticsFactory.cs | 3 +- .../Game/GameFileOperationException.cs | 37 +++ .../Snap.Hutao/Service/Game/GameService.cs | 62 +++- .../Snap.Hutao/Service/Game/IGameService.cs | 12 +- .../Service/Game/Package/ConvertDirection.cs | 20 ++ .../Service/Game/Package/ItemOperationInfo.cs | 53 +++ .../Service/Game/Package/ItemOperationType.cs | 25 ++ .../Service/Game/Package/PackageConverter.cs | 274 +++++++++++++++ .../Game/Package/PackageReplaceStatus.cs | 24 ++ .../Service/Game/Package/VersionItem.cs | 28 ++ .../Service/Metadata/MetadataService.cs | 18 +- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 16 +- .../View/Control/StatisticsCard.xaml | 11 +- .../LaunchGamePackageConvertDialog.xaml | 25 ++ .../LaunchGamePackageConvertDialog.xaml.cs | 35 ++ .../Snap.Hutao/View/Page/CultivationPage.xaml | 17 +- .../Snap.Hutao/View/Page/GachaLogPage.xaml | 8 +- .../View/Page/HutaoDatabasePage.xaml | 1 - .../View/Page/HutaoDatabasePresentPage.xaml | 311 ++++++++++++++++++ .../Page/HutaoDatabasePresentPage.xaml.cs | 23 ++ .../Snap.Hutao/View/Page/LaunchGamePage.xaml | 55 +++- .../Snap.Hutao/View/Page/SettingPage.xaml | 7 +- .../Snap.Hutao/View/Page/TestPage.xaml | 4 + src/Snap.Hutao/Snap.Hutao/View/UserView.xaml | 6 +- .../Snap.Hutao/View/WelcomeView.xaml | 5 +- .../ViewModel/AvatarPropertyViewModel.cs | 2 +- .../ViewModel/HutaoDatabaseViewModel.cs | 37 ++- .../ViewModel/LaunchGameViewModel.cs | 57 ++-- .../Snap.Hutao/ViewModel/TestViewModel.cs | 13 + .../Snap.Hutao/ViewModel/WelcomeViewModel.cs | 11 +- src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs | 8 +- .../Snap.Hutao/Web/ApiOsEndpoints.cs | 16 + .../SdkStatic/Hk4e/Launcher/GameResource.cs | 4 +- .../SdkStatic/Hk4e/Launcher/Package.cs | 12 +- .../SdkStatic/Hk4e/Launcher/ResourceClient.cs | 5 +- .../Hoyolab/SdkStatic/Hk4e/Launcher/Sdk.cs | 28 ++ .../Web/Response/KnownReturnCode.cs | 10 + 48 files changed, 1267 insertions(+), 126 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/IO/FileDigest.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileOperationException.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ConvertDirection.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationType.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Service/Game/Package/VersionItem.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePresentPage.xaml create mode 100644 src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePresentPage.xaml.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Sdk.cs diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/Program.cs b/src/Snap.Hutao/Snap.Hutao.Installer/Program.cs index 5478278e..9d7d0a42 100644 --- a/src/Snap.Hutao/Snap.Hutao.Installer/Program.cs +++ b/src/Snap.Hutao/Snap.Hutao.Installer/Program.cs @@ -8,7 +8,7 @@ namespace Snap.Hutao.Installer; internal class Program { - private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows\Appx"; + private const string AppxKey = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock"; private const string ValueName = "AllowDevelopmentWithoutDevLicense"; public static async Task Main(string[] args) diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs index 80801cdf..a1a88d06 100644 --- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs +++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs @@ -25,7 +25,6 @@ public partial class App : Application /// Initializes the singleton application object. /// /// 日志器 - /// App Center public App(ILogger logger) { // load app resource diff --git a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs index 8ca741d4..ca944b11 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/DependencyInjection/IocConfiguration.cs @@ -38,7 +38,9 @@ internal static class IocConfiguration { if (context.Database.GetPendingMigrations().Any()) { +#if DEBUG Debug.WriteLine("[Debug] Performing AppDbContext Migrations"); +#endif context.Database.Migrate(); } } diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/FileDigest.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/FileDigest.cs new file mode 100644 index 00000000..a59a9407 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/FileDigest.cs @@ -0,0 +1,31 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.IO; +using System.Security.Cryptography; + +namespace Snap.Hutao.Core.IO; + +/// +/// 文件摘要 +/// +internal static class FileDigest +{ + /// + /// 异步获取文件 Md5 摘要 + /// + /// 文件路径 + /// 取消令牌 + /// 文件 Md5 摘要 + public static async Task GetMd5Async(string filePath, CancellationToken token) + { + using (FileStream stream = File.OpenRead(filePath)) + { + using (MD5 md5 = MD5.Create()) + { + byte[] bytes = await md5.ComputeHashAsync(stream, token).ConfigureAwait(false); + return System.Convert.ToHexString(bytes); + } + } + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/IO/FilePath.cs b/src/Snap.Hutao/Snap.Hutao/Core/IO/FilePath.cs index 84f3827b..692e33df 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/IO/FilePath.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/IO/FilePath.cs @@ -109,4 +109,4 @@ internal readonly struct FilePath : IEquatable { return Value.GetHashCode(); } -} +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs index bbc79453..43f66722 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/SettingKeys.cs @@ -24,7 +24,10 @@ internal static class SettingKeys public const string LaunchTimes = "LaunchTimes"; /// - /// 静态资源合约V1 + /// 静态资源合约 + /// 新增合约时 请注意 + /// + /// 与 /// public const string StaticResourceV1Contract = "StaticResourceV1Contract"; @@ -37,4 +40,9 @@ internal static class SettingKeys /// 静态资源合约V3 刷新 Skill Talent /// public const string StaticResourceV3Contract = "StaticResourceV3Contract"; + + /// + /// 静态资源合约V4 刷新 AvatarIcon + /// + public const string StaticResourceV4Contract = "StaticResourceV4Contract"; } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/StaticResource.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/StaticResource.cs index ae0e6842..4791a3e9 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/StaticResource.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/StaticResource.cs @@ -8,6 +8,17 @@ namespace Snap.Hutao.Core.Setting; /// internal static class StaticResource { + /// + /// 完成所有合约 + /// + public static void FulfillAllContracts() + { + LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true); + LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true); + LocalSetting.Set(SettingKeys.StaticResourceV3Contract, true); + LocalSetting.Set(SettingKeys.StaticResourceV4Contract, true); + } + /// /// 提供的合约是否未完成 /// @@ -26,6 +37,7 @@ internal static class StaticResource { return !LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false) || (!LocalSetting.Get(SettingKeys.StaticResourceV2Contract, false)) - || (!LocalSetting.Get(SettingKeys.StaticResourceV3Contract, false)); + || (!LocalSetting.Get(SettingKeys.StaticResourceV3Contract, false)) + || (!LocalSetting.Get(SettingKeys.StaticResourceV4Contract, false)); } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Binding/LaunchGame/LaunchScheme.cs b/src/Snap.Hutao/Snap.Hutao/Model/Binding/LaunchGame/LaunchScheme.cs index e1d8adf9..0c5db92e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Binding/LaunchGame/LaunchScheme.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Binding/LaunchGame/LaunchScheme.cs @@ -1,36 +1,45 @@ // Copyright (c) DGP Studio. All rights reserved. // Licensed under the MIT license. +using System.Collections.Immutable; + namespace Snap.Hutao.Model.Binding.LaunchGame; -/// -/// 服务器方案 -/// /// /// 启动方案 /// public class LaunchScheme { + /// + /// 已知的启动方案 + /// + public static readonly ImmutableList KnownSchemes = new List() + { + new LaunchScheme("官方服 | 天空岛", "eYd89JmJ", "18", "1", "1"), + new LaunchScheme("渠道服 | 世界树", "KAtdSsoQ", "17", "14", "0"), + new LaunchScheme("国际服 | 部分支持", "gcStgarh", "10", "1", "0"), + }.ToImmutableList(); + /// /// 构造一个新的启动方案 /// - /// 名称 + /// 名称 /// 通道 - /// 通道描述字符串 /// 子通道 /// 启动器Id - public LaunchScheme(string name, string channel, string subChannel, string launcherId) + private LaunchScheme(string displayName, string key, string launcherId, string channel, string subChannel) { - Name = name; + DisplayName = displayName; Channel = channel; SubChannel = subChannel; LauncherId = launcherId; + Key = key; } /// /// 名称 /// - public string Name { get; set; } + public string DisplayName { get; set; } /// /// 通道 @@ -46,4 +55,14 @@ public class LaunchScheme /// 启动器Id /// public string LauncherId { get; set; } + + /// + /// API Key + /// + public string Key { get; set; } + + /// + /// 是否为海外 + /// + public bool IsOversea { get => LauncherId == "10"; } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/AvatarIds.cs b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/AvatarIds.cs index 37551378..0c11f31f 100644 --- a/src/Snap.Hutao/Snap.Hutao/Model/Metadata/AvatarIds.cs +++ b/src/Snap.Hutao/Snap.Hutao/Model/Metadata/AvatarIds.cs @@ -101,12 +101,14 @@ public static class AvatarIds { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", + SideIcon = "UI_AvatarIcon_Side_PlayerBoy", Quality = Intrinsic.ItemQuality.QUALITY_ORANGE, }, [PlayerGirl] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", + SideIcon = "UI_AvatarIcon_Side_PlayerGirl", Quality = Intrinsic.ItemQuality.QUALITY_ORANGE, }, }; diff --git a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest index 5c038e20..fb83f62e 100644 --- a/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest +++ b/src/Snap.Hutao/Snap.Hutao/Package.appxmanifest @@ -11,7 +11,7 @@ @@ -24,7 +24,7 @@ - + diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs index 27d7c698..11eac584 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsExtensions.cs @@ -74,11 +74,8 @@ public static class GachaStatisticsExtensions private static Color GetColorByName(string name) { - byte[] codes = MD5.HashData(Encoding.UTF8.GetBytes(name)); - Span first = new(codes, 0, 5); - Span second = new(codes, 5, 5); - Span third = new(codes, 10, 5); - Color color = Color.FromArgb(255, first.Average(), second.Average(), third.Average()); + Span codes = MD5.HashData(Encoding.UTF8.GetBytes(name)); + Color color = Color.FromArgb(255, codes.Slice(0, 5).Average(), codes.Slice(5, 5).Average(), codes.Slice(10, 5).Average()); return color; } } diff --git a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs index 6f68928c..4b08bb1d 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/GachaLog/Factory/GachaStatisticsFactory.cs @@ -54,7 +54,8 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory bool isEmptyHistoryWishVisible = entry.GetBoolean(); IOrderedEnumerable orderedItems = items.OrderBy(i => i.Id); - return await Task.Run(() => CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap, isEmptyHistoryWishVisible)).ConfigureAwait(false); + await ThreadHelper.SwitchToBackgroundAsync(); + return CreateCore(orderedItems, historyWishBuilders, idAvatarMap, idWeaponMap, isEmptyHistoryWishVisible); } private static GachaStatistics CreateCore( diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileOperationException.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileOperationException.cs new file mode 100644 index 00000000..f388e6bb --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameFileOperationException.cs @@ -0,0 +1,37 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; +using Snap.Hutao.Core; +using Snap.Hutao.Core.Database; +using Snap.Hutao.Core.IO.Ini; +using Snap.Hutao.Extension; +using Snap.Hutao.Model.Binding.LaunchGame; +using Snap.Hutao.Model.Entity; +using Snap.Hutao.Model.Entity.Database; +using Snap.Hutao.Service.Game.Locator; +using Snap.Hutao.Service.Game.Unlocker; +using Snap.Hutao.View.Dialog; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.IO; + +namespace Snap.Hutao.Service.Game; + +/// +/// 游戏文件操作异常 +/// +internal class GameFileOperationException : Exception +{ + /// + /// 构造一个新的用户数据损坏异常 + /// + /// 消息 + /// 内部错误 + public GameFileOperationException(string message, Exception innerException) + : base($"游戏文件操作失败: {message}", innerException) + { + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs index c86e24d0..0dc89243 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/GameService.cs @@ -12,8 +12,11 @@ using Snap.Hutao.Model.Binding.LaunchGame; using Snap.Hutao.Model.Entity; using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Service.Game.Locator; +using Snap.Hutao.Service.Game.Package; using Snap.Hutao.Service.Game.Unlocker; using Snap.Hutao.View.Dialog; +using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; +using Snap.Hutao.Web.Response; using System.Collections.ObjectModel; using System.Diagnostics; using System.IO; @@ -24,13 +27,15 @@ namespace Snap.Hutao.Service.Game; /// 游戏服务 /// [Injection(InjectAs.Singleton, typeof(IGameService))] -internal class GameService : IGameService, IDisposable +[SuppressMessage("", "CA1001")] +internal class GameService : IGameService { private const string GamePathKey = $"{nameof(GameService)}.Cache.{SettingEntry.GamePath}"; private const string ConfigFile = "config.ini"; private readonly IServiceScopeFactory scopeFactory; private readonly IMemoryCache memoryCache; + private readonly PackageConverter packageConverter; private readonly SemaphoreSlim gameSemaphore = new(1); private ObservableCollection? gameAccounts; @@ -40,10 +45,12 @@ internal class GameService : IGameService, IDisposable /// /// 范围工厂 /// 内存缓存 - public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache) + /// 游戏文件包转换器 + public GameService(IServiceScopeFactory scopeFactory, IMemoryCache memoryCache, PackageConverter packageConverter) { this.scopeFactory = scopeFactory; this.memoryCache = memoryCache; + this.packageConverter = packageConverter; } /// @@ -159,15 +166,26 @@ internal class GameService : IGameService, IDisposable } /// - public void SetMultiChannel(LaunchScheme scheme) + public bool SetMultiChannel(LaunchScheme scheme) { string gamePath = GetGamePathSkipLocator(); string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile); List elements; - using (FileStream readStream = File.OpenRead(configPath)) + try { - elements = IniSerializer.Deserialize(readStream).ToList(); + using (FileStream readStream = File.OpenRead(configPath)) + { + elements = IniSerializer.Deserialize(readStream).ToList(); + } + } + catch (DirectoryNotFoundException dnfEx) + { + throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", dnfEx); + } + catch (UnauthorizedAccessException uaEx) + { + throw new GameFileOperationException($"无法读取或保存配置文件,请以管理员模式重试。", uaEx); } bool changed = false; @@ -203,10 +221,36 @@ internal class GameService : IGameService, IDisposable IniSerializer.Serialize(writeStream, elements); } } + + return changed; + } + + /// + public async Task ReplaceGameResourceAsync(LaunchScheme launchScheme, IProgress progress) + { + string gamePath = GetGamePathSkipLocator(); + string gameFolder = Path.GetDirectoryName(gamePath)!; + string gameFileName = Path.GetFileName(gamePath); + + if (launchScheme.IsOversea && gameFileName == "GenshinImpact.exe") + { + // Already that scheme, no need to replace files + return true; + } + else if (!launchScheme.IsOversea && gameFileName == "YuanShen.exe") + { + // Already that scheme, no need to replace files + return true; + } + + // TODO: we still need to handle the Bilibili scheme. + await packageConverter.ReplaceGameResourceAsync(launchScheme, gameFolder, progress).ConfigureAwait(false); + // We need to change the gamePath if we switch. + + return true; } /// - [SuppressMessage("", "IDE0046")] public bool IsGameRunning() { if (gameSemaphore.CurrentCount == 0) @@ -387,10 +431,4 @@ internal class GameService : IGameService, IDisposable await scope.ServiceProvider.GetRequiredService().GameAccounts.RemoveAndSaveAsync(gameAccount).ConfigureAwait(false); } } - - /// - public void Dispose() - { - gameSemaphore?.Dispose(); - } } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs index b703f0a2..13c01785 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/IGameService.cs @@ -3,6 +3,7 @@ using Snap.Hutao.Model.Binding.LaunchGame; using Snap.Hutao.Model.Entity; +using Snap.Hutao.Service.Game.Package; using System.Collections.ObjectModel; namespace Snap.Hutao.Service.Game; @@ -83,6 +84,14 @@ internal interface IGameService /// 任务 ValueTask RemoveGameAccountAsync(GameAccount gameAccount); + /// + /// 替换游戏资源 + /// + /// 目标启动方案 + /// 进度 + /// 是否替换成功 + Task ReplaceGameResourceAsync(LaunchScheme launchScheme, IProgress progress); + /// /// 修改注册表中的账号信息 /// @@ -94,5 +103,6 @@ internal interface IGameService /// 设置多通道值 /// /// 方案 - void SetMultiChannel(LaunchScheme scheme); + /// 是否更改了ini文件 + bool SetMultiChannel(LaunchScheme scheme); } \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ConvertDirection.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ConvertDirection.cs new file mode 100644 index 00000000..d0e27704 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ConvertDirection.cs @@ -0,0 +1,20 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Package; + +/// +/// 转换方向 +/// +internal enum ConvertDirection +{ + /// + /// 国际服转国服 + /// + OverseaToChinese, + + /// + /// 国服转国际服 + /// + ChineseToOversea, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs new file mode 100644 index 00000000..63305b2c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationInfo.cs @@ -0,0 +1,53 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using System.Diagnostics; + +namespace Snap.Hutao.Service.Game.Package; + +/// +/// 包操作 +/// +[DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")] +internal class ItemOperationInfo +{ + /// + /// 构造一个新的包操作 + /// + /// 操作类型 + /// 目标 + /// 缓存 + public ItemOperationInfo(ItemOperationType type, VersionItem target, VersionItem cache) + { + Type = type; + Target = target.RemoteName; + Cache = cache.RemoteName; + Md5 = target.Md5; + TotalBytes = target.FileSize; + } + + /// + /// 操作的类型 + /// + public ItemOperationType Type { get; set; } + + /// + /// 目标文件 + /// + public string Target { get; set; } + + /// + /// 移动至中时的名称 + /// + public string Cache { get; set; } + + /// + /// 文件的目标Md5 + /// + public string Md5 { get; set; } + + /// + /// 文件的目标大小 Byte + /// + public long TotalBytes { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationType.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationType.cs new file mode 100644 index 00000000..99ce606b --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/ItemOperationType.cs @@ -0,0 +1,25 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Package; + +/// +/// 包文件操作的类型 +/// +internal enum ItemOperationType +{ + /// + /// 添加 + /// + Add, + + /// + /// 删除 + /// + Remove, + + /// + /// 替换 + /// + Replace, +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs new file mode 100644 index 00000000..9940ed2c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageConverter.cs @@ -0,0 +1,274 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; +using Snap.Hutao.Core.IO; +using Snap.Hutao.Model.Binding.LaunchGame; +using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher; +using Snap.Hutao.Web.Response; +using System.IO; +using System.Net.Http; + +namespace Snap.Hutao.Service.Game.Package; + +/// +/// 游戏文件包转换器 +/// +[HttpClient(HttpClientConfigration.Default)] +internal class PackageConverter +{ + private const string GenshinImpactData = "GenshinImpact_Data"; + private const string YuanShenData = "YuanShen_Data"; + + private readonly ResourceClient resourceClient; + private readonly JsonSerializerOptions options; + private readonly HttpClient httpClient; + + /// + /// 构造一个新的游戏文件转换器 + /// + /// 资源客户端 + /// Json序列化选项 + /// http客户端 + public PackageConverter(ResourceClient resourceClient, JsonSerializerOptions options, HttpClient httpClient) + { + this.resourceClient = resourceClient; + this.options = options; + this.httpClient = httpClient; + } + + /// + /// 异步替换游戏资源 + /// 调用前需要确认本地文件与服务器上的不同 + /// + /// 目标启动方案 + /// 游戏目录 + /// 进度 + /// 任务 + public async Task ReplaceGameResourceAsync(LaunchScheme targetScheme, string gameFolder, IProgress progress) + { + await ThreadHelper.SwitchToBackgroundAsync(); + progress.Report(new("查询游戏资源信息")); + Response response = await resourceClient.GetResourceAsync(targetScheme).ConfigureAwait(false); + + if (response.IsOk()) + { + GameResource remoteGameResouce = response.Data; + + string scatteredFilesUrl = remoteGameResouce.Game.Latest.DecompressedPath; + Uri pkgVersionUri = new($"{scatteredFilesUrl}/pkg_version"); + ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese; + + progress.Report(new("下载包版本信息")); + Dictionary remoteItems; + using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false)) + { + remoteItems = await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false); + } + + Dictionary localItems; + using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, "pkg_version"))) + { + localItems = await GetVersionItemsAsync(localSteam, direction, ConvertRemoteName).ConfigureAwait(false); + } + + IEnumerable diffOperations = GetItemOperationInfos(remoteItems, localItems); + var a = diffOperations.ToList(); + await ReplaceGameResourceCoreAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false); + return true; + } + + return false; + } + + private static string ConvertRemoteName(string remoteName, ConvertDirection direction) + { + // 我们已经提前重命名了整个 Data 文件夹 所以需要将 RemoteName 中的 Data 同样替换 + if (remoteName.StartsWith(YuanShenData) || remoteName.StartsWith(GenshinImpactData)) + { + return direction switch + { + ConvertDirection.OverseaToChinese => $"{YuanShenData}{remoteName[GenshinImpactData.Length..]}", + ConvertDirection.ChineseToOversea => $"{GenshinImpactData}{remoteName[YuanShenData.Length..]}", + _ => remoteName, + }; + } + + return remoteName; + } + + private static IEnumerable GetItemOperationInfos(Dictionary remote, Dictionary local) + { + foreach ((string remoteName, VersionItem remoteItem) in remote) + { + if (local.TryGetValue(remoteName, out VersionItem? localItem)) + { + if (remoteItem.Md5 != localItem.Md5) + { + // 本地发现了同名且不同MD5的项,需要替换为服务器上的项 + yield return new(ItemOperationType.Replace, remoteItem, localItem); + } + + local.Remove(remoteName); + } + else + { + // 本地没有发现同名项 + yield return new(ItemOperationType.Add, remoteItem, remoteItem); + } + } + + IEnumerable removes = local.Select(kvp => new ItemOperationInfo(ItemOperationType.Remove, kvp.Value, kvp.Value)); + + foreach (ItemOperationInfo item in removes) + { + yield return item; + } + } + + private static void RenameDataFolder(string gameFolder, ConvertDirection direction) + { + string yuanShenData = Path.Combine(gameFolder, YuanShenData); + string genshinImpactData = Path.Combine(gameFolder, GenshinImpactData); + + // We have check the exe path previously + // so we assume the data folder is present + if (direction == ConvertDirection.ChineseToOversea) + { + Directory.Move(yuanShenData, genshinImpactData); + } + else + { + Directory.Move(genshinImpactData, yuanShenData); + } + } + + private static void MoveToCache(string cacheFolder, string cacheName, string targetFullPath) + { + string cacheFilePath = Path.Combine(cacheFolder, cacheName); + Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!); + File.Move(targetFullPath, cacheFilePath, true); + } + + private async Task ReplaceGameResourceCoreAsync(IEnumerable operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress progress) + { + // 重命名 _Data 目录 + RenameDataFolder(gameFolder, direction); + + // Ensure cache folder + string cacheFolder = Path.Combine(gameFolder, "Screenshot", "HutaoCache"); + + // 执行下载与移动操作 + foreach (ItemOperationInfo info in operations) + { + progress.Report(new($"{info.Target}")); + + string targetFilePath = Path.Combine(gameFolder, info.Target); + string cacheFilePath = Path.Combine(cacheFolder, info.Cache); + + switch (info.Type) + { + case ItemOperationType.Add: + await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info).ConfigureAwait(false); + break; + case ItemOperationType.Replace: + { + MoveToCache(cacheFolder, info.Cache, targetFilePath); + await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info).ConfigureAwait(false); + + break; + } + + case ItemOperationType.Remove: + MoveToCache(cacheFolder, info.Cache, targetFilePath); + break; + + default: + break; + } + } + + // 重新下载所有 *pkg_version 文件 + await ReplacePackageVersionsAsync(scatteredFilesUrl, gameFolder).ConfigureAwait(false); + } + + private async Task ReplaceFromCacheOrWebAsync(string cacheFilePath, string targetFilePath, string scatteredFilesUrl, ItemOperationInfo info) + { + if (File.Exists(cacheFilePath)) + { + string remoteMd5 = await FileDigest.GetMd5Async(cacheFilePath, CancellationToken.None).ConfigureAwait(false); + if (info.Md5 == remoteMd5.ToLowerInvariant() && new FileInfo(cacheFilePath).Length == info.TotalBytes) + { + // Valid, move it to target path + // There shouldn't be any file in the same name + File.Move(cacheFilePath, targetFilePath, false); + return; + } + else + { + // Invalid file, delete it + File.Delete(cacheFilePath); + } + } + + // Cache no item, download it anyway. + using (FileStream fileStream = File.Create(targetFilePath)) + { + using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{info.Target}").ConfigureAwait(false)) + { + await webStream.CopyToAsync(fileStream).ConfigureAwait(false); + } + } + } + + private async Task ReplacePackageVersionsAsync(string scatteredFilesUrl, string gameFolder) + { + foreach (string audioPkgVersionFilePath in Directory.EnumerateFiles(gameFolder, "*pkg_version")) + { + string audioPkgVersionFileName = Path.GetFileName(audioPkgVersionFilePath); + using (FileStream audioPkgVersionFileStream = File.Create(audioPkgVersionFilePath)) + { + using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{audioPkgVersionFileName}").ConfigureAwait(false)) + { + await webStream.CopyToAsync(audioPkgVersionFileStream).ConfigureAwait(false); + } + } + } + } + + private async Task> GetVersionItemsAsync(Stream stream) + { + Dictionary results = new(); + using (StreamReader reader = new(stream)) + { + while (await reader.ReadLineAsync().ConfigureAwait(false) is string raw) + { + if (!string.IsNullOrEmpty(raw)) + { + VersionItem item = JsonSerializer.Deserialize(raw, options)!; + results.Add(item.RemoteName, item); + } + } + } + + return results; + } + + private async Task> GetVersionItemsAsync(Stream stream, ConvertDirection direction, Func nameConverter) + { + Dictionary results = new(); + using (StreamReader reader = new(stream)) + { + while (await reader.ReadLineAsync().ConfigureAwait(false) is string raw) + { + if (!string.IsNullOrEmpty(raw)) + { + VersionItem item = JsonSerializer.Deserialize(raw, options)!; + results.Add(nameConverter(item.RemoteName, direction), item); + } + } + } + + return results; + } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs new file mode 100644 index 00000000..6398abbe --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/PackageReplaceStatus.cs @@ -0,0 +1,24 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Package; + +/// +/// 包更新状态 +/// +public class PackageReplaceStatus +{ + /// + /// 构造一个新的包更新状态 + /// + /// 描述 + public PackageReplaceStatus(string description) + { + Description = description; + } + + /// + /// 描述 + /// + public string Description { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/VersionItem.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/VersionItem.cs new file mode 100644 index 00000000..6837fa80 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Package/VersionItem.cs @@ -0,0 +1,28 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +namespace Snap.Hutao.Service.Game.Package; + +/// +/// 包版本项 +/// +internal class VersionItem +{ + /// + /// 服务器上的名称 + /// + [JsonPropertyName("remoteName")] + public string RemoteName { get; set; } = default!; + + /// + /// MD5校验值 + /// + [JsonPropertyName("md5")] + public string Md5 { get; set; } = default!; + + /// + /// 文件尺寸 + /// + [JsonPropertyName("fileSize")] + public long FileSize { get; set; } +} \ No newline at end of file diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs index f9c4a6f5..fc630bf5 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataService.cs @@ -4,6 +4,7 @@ using Microsoft.Extensions.Caching.Memory; using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient; using Snap.Hutao.Core.Diagnostics; +using Snap.Hutao.Core.IO; using Snap.Hutao.Core.Logging; using Snap.Hutao.Extension; using Snap.Hutao.Service.Abstraction; @@ -131,9 +132,10 @@ internal partial class MetadataService : IMetadataService, IMetadataServiceIniti string fileFullName = $"{fileName}.json"; bool skip = false; - if (File.Exists(Path.Combine(metadataFolderPath, fileFullName))) + string fileFullPath = Path.Combine(metadataFolderPath, fileFullName); + if (File.Exists(fileFullPath)) { - skip = md5 == await GetFileMd5Async(fileFullName, token).ConfigureAwait(false); + skip = md5 == await FileDigest.GetMd5Async(fileFullPath, token).ConfigureAwait(false); } if (!skip) @@ -145,18 +147,6 @@ internal partial class MetadataService : IMetadataService, IMetadataServiceIniti }); } - private async Task GetFileMd5Async(string fileFullName, CancellationToken token) - { - using (FileStream stream = File.OpenRead(Path.Combine(metadataFolderPath, fileFullName))) - { - byte[] bytes = await MD5.Create() - .ComputeHashAsync(stream, token) - .ConfigureAwait(false); - - return Convert.ToHexString(bytes); - } - } - private async Task DownloadMetadataAsync(string fileFullName, CancellationToken token) { Stream sourceStream = await httpClient diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj index 6b5ffe17..f6582f59 100644 --- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj +++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj @@ -87,6 +87,7 @@ + @@ -99,6 +100,7 @@ + @@ -171,11 +173,11 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all @@ -199,6 +201,16 @@ + + + MSBuild:Compile + + + + + MSBuild:Compile + + MSBuild:Compile diff --git a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml index 67a1e04b..60bfd7d7 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Control/StatisticsCard.xaml @@ -2,6 +2,7 @@ x:Class="Snap.Hutao.View.Control.StatisticsCard" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" + xmlns:converters="using:CommunityToolkit.WinUI.UI.Converters" xmlns:cwucont="using:CommunityToolkit.WinUI.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" @@ -62,9 +63,17 @@ + + - + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs new file mode 100644 index 00000000..e3d8bc20 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Dialog/LaunchGamePackageConvertDialog.xaml.cs @@ -0,0 +1,35 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Snap.Hutao.Control; + +namespace Snap.Hutao.View.Dialog; + +/// +/// ϷͻתԻ +/// +public sealed partial class LaunchGamePackageConvertDialog : ContentDialog +{ + private static readonly DependencyProperty DescriptionProperty = Property.Depend(nameof(Description), "Ժ"); + + /// + /// һµϷͻתԻ + /// + public LaunchGamePackageConvertDialog() + { + InitializeComponent(); + XamlRoot = Ioc.Default.GetRequiredService().Content.XamlRoot; + DataContext = this; + } + + /// + /// + /// + public string Description + { + get { return (string)GetValue(DescriptionProperty); } + set { SetValue(DescriptionProperty, value); } + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml index 2f36441b..4b708439 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/CultivationPage.xaml @@ -138,8 +138,9 @@ + - + @@ -164,14 +165,20 @@ Width="48" Height="48" Margin="8,0,0,0" + Background="Transparent" Command="{Binding Path=DataContext.RemoveEntryCommand, Source={StaticResource BindingProxy}}" CommandParameter="{Binding}" Content="" FontFamily="{StaticResource SymbolThemeFontFamily}" + Style="{StaticResource ButtonRevealStyle}" ToolTipService.ToolTip="删除清单"/> - + + @@ -201,10 +208,9 @@ HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" Background="Transparent" - BorderBrush="{x:Null}" - BorderThickness="0" Command="{Binding Path=DataContext.FinishStateCommand, Source={StaticResource BindingProxy}}" - CommandParameter="{Binding}"> + CommandParameter="{Binding}" + Style="{StaticResource ButtonRevealStyle}"> @@ -225,7 +231,6 @@ Text="{Binding Entity.Count}"/> - diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml index 8856eb2e..860031bd 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/GachaLogPage.xaml @@ -26,7 +26,7 @@ @@ -86,9 +86,9 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePresentPage.xaml.cs b/src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePresentPage.xaml.cs new file mode 100644 index 00000000..77705558 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/HutaoDatabasePresentPage.xaml.cs @@ -0,0 +1,23 @@ +// Copyright (c) DGP Studio. All rights reserved. +// Licensed under the MIT license. + +using Snap.Hutao.Control; +using Snap.Hutao.ViewModel; + +namespace Snap.Hutao.View.Page; + +/// +/// 用于展示用途的胡桃数据库页面 +/// 仅用于发布相关的统计数据 +/// +public sealed partial class HutaoDatabasePresentPage : ScopedPage +{ + /// + /// 构造一个新的胡桃数据库页面 + /// + public HutaoDatabasePresentPage() + { + InitializeComponent(); + InitializeWith(); + } +} diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml index 1c8a7e70..0c016f7f 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/LaunchGamePage.xaml @@ -32,7 +32,7 @@ @@ -46,7 +46,12 @@ - + + @@ -198,7 +203,7 @@ Header="宽度" Icon=""> - + - + - + + + + + + + + - + + + + + + + - @@ -130,7 +129,7 @@ - + diff --git a/src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml b/src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml index 0a2af1be..ed8945aa 100644 --- a/src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml +++ b/src/Snap.Hutao/Snap.Hutao/View/Page/TestPage.xaml @@ -32,6 +32,10 @@