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 @@
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
index c80b5603..0dd4be5a 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/UserView.xaml
@@ -129,24 +129,22 @@
Margin="12,0,0,0"
VerticalAlignment="Stretch"
Background="Transparent"
- BorderBrush="{x:Null}"
- BorderThickness="0"
Command="{Binding DataContext.CopyCookieCommand, Source={StaticResource ViewModelBindingProxy}}"
CommandParameter="{Binding}"
Content=""
FontFamily="{StaticResource SymbolThemeFontFamily}"
+ Style="{StaticResource ButtonRevealStyle}"
ToolTipService.ToolTip="复制 Cookie"/>
diff --git a/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml b/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml
index afc5ec55..5088b8ce 100644
--- a/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/View/WelcomeView.xaml
@@ -2,7 +2,6 @@
x:Class="Snap.Hutao.View.WelcomeView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
@@ -32,6 +31,10 @@
Style="{StaticResource BodyTextBlockStyle}"
Text="你可以继续使用电脑,丝毫不受影响"/>
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs
index 188c7420..d3cb14fc 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/AvatarPropertyViewModel.cs
@@ -280,7 +280,7 @@ internal class AvatarPropertyViewModel : Abstraction.ViewModel
IBuffer buffer = await bitmap.GetPixelsAsync();
bool clipboardOpened = false;
- using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight, BitmapAlphaMode.Ignore))
+ using (SoftwareBitmap softwareBitmap = SoftwareBitmap.CreateCopyFromBuffer(buffer, BitmapPixelFormat.Bgra8, bitmap.PixelWidth, bitmap.PixelHeight))
{
Color tintColor = (Color)Ioc.Default.GetRequiredService().Resources["CompatBackgroundColor"];
Bgra8 tint = Bgra8.FromColor(tintColor);
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs
index f5fa0da4..754ea9c3 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/HutaoDatabaseViewModel.cs
@@ -2,9 +2,15 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Input;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Web.Hutao.Model;
+using System.IO;
+using System.Runtime.InteropServices.WindowsRuntime;
+using Windows.Graphics.Imaging;
+using Windows.Storage.Streams;
namespace Snap.Hutao.ViewModel;
@@ -31,6 +37,7 @@ internal class HutaoDatabaseViewModel : Abstraction.ViewModel
this.hutaoCache = hutaoCache;
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
+ ExportAsImageCommand = new AsyncRelayCommand(ExportAsImageAsync);
}
///
@@ -46,7 +53,7 @@ internal class HutaoDatabaseViewModel : Abstraction.ViewModel
///
/// 角色命座信息
///
- public List? AvatarConstellationInfos { get => avatarConstellationInfos; set => avatarConstellationInfos = value; }
+ public List? AvatarConstellationInfos { get => avatarConstellationInfos; set => SetProperty(ref avatarConstellationInfos, value); }
///
/// 队伍出场
@@ -63,6 +70,11 @@ internal class HutaoDatabaseViewModel : Abstraction.ViewModel
///
public ICommand OpenUICommand { get; }
+ ///
+ /// 导出为图片命令
+ ///
+ public ICommand ExportAsImageCommand { get; }
+
private async Task OpenUIAsync()
{
if (await hutaoCache.InitializeForDatabaseViewModelAsync().ConfigureAwait(true))
@@ -74,4 +86,27 @@ internal class HutaoDatabaseViewModel : Abstraction.ViewModel
Overview = hutaoCache.Overview;
}
}
+
+ private async Task ExportAsImageAsync(UIElement? element)
+ {
+ if (element == null)
+ {
+ return;
+ }
+
+ RenderTargetBitmap bitmap = new();
+ await bitmap.RenderAsync(element);
+
+ IBuffer buffer = await bitmap.GetPixelsAsync();
+ string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
+ using (FileStream file = File.Create(Path.Combine(desktop, "hutao-database.png")))
+ {
+ using (IRandomAccessStream randomFileStream = file.AsRandomAccessStream())
+ {
+ BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, randomFileStream);
+ encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Straight, (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight, 96, 96, buffer.ToArray());
+ await encoder.FlushAsync();
+ }
+ }
+ }
}
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs
index 7598df05..a18ea2cc 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/LaunchGameViewModel.cs
@@ -4,6 +4,7 @@
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;
+using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Model.Binding.LaunchGame;
@@ -13,6 +14,7 @@ using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.User;
+using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using System.IO;
@@ -37,12 +39,7 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
private readonly AppDbContext appDbContext;
private readonly IMemoryCache memoryCache;
- private readonly List knownSchemes = new()
- {
- new LaunchScheme(name: "官方服 | 天空岛", channel: "1", subChannel: "1", launcherId: "18"),
- new LaunchScheme(name: "渠道服 | 世界树", channel: "14", subChannel: "0", launcherId: "17"),
- new LaunchScheme(name: "国际服 | 部分支持", channel: "1", subChannel: "0", launcherId: "unknown"),
- };
+ private readonly List knownSchemes = LaunchScheme.KnownSchemes.ToList();
private LaunchScheme? selectedScheme;
private ObservableCollection? gameAccounts;
@@ -301,30 +298,38 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
{
try
{
- gameService.SetMultiChannel(SelectedScheme);
+ if (gameService.SetMultiChannel(SelectedScheme))
+ {
+ // Channel changed, we need to change local file.
+ // Note that if channel changed successfully, the
+ // access level is already high enough.
+ await ThreadHelper.SwitchToMainThreadAsync();
+ LaunchGamePackageConvertDialog dialog = new();
+ await using (await dialog.BlockAsync().ConfigureAwait(false))
+ {
+ Progress progress = new(s => dialog.Description = s.Description);
+ await gameService.ReplaceGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false);
+ }
+ }
+
+ if (SelectedGameAccount != null)
+ {
+ if (!gameService.SetGameAccount(SelectedGameAccount))
+ {
+ infoBarService.Warning("切换账号失败");
+ }
+ }
+
+ SaveSetting();
+
+ LaunchConfiguration configuration = new(IsExclusive, IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps);
+ await gameService.LaunchAsync(configuration).ConfigureAwait(false);
}
- catch (DirectoryNotFoundException)
+ catch (GameFileOperationException ex)
{
- infoBarService.Warning("找不到游戏配置文件 config.ini");
- }
- catch (UnauthorizedAccessException)
- {
- infoBarService.Warning("无法读取或保存配置文件,请以管理员模式启动胡桃。");
+ infoBarService.Error(ex);
}
}
-
- if (SelectedGameAccount != null)
- {
- if (!gameService.SetGameAccount(SelectedGameAccount))
- {
- infoBarService.Warning("切换账号失败");
- }
- }
-
- SaveSetting();
-
- LaunchConfiguration configuration = new(IsExclusive, IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps);
- await gameService.LaunchAsync(configuration).ConfigureAwait(false);
}
private async Task DetectGameAccountAsync()
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs
index c19b1385..29f3f76c 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/TestViewModel.cs
@@ -27,6 +27,7 @@ internal class TestViewModel : Abstraction.ViewModel
ShowAdoptCalculatorDialogCommand = new AsyncRelayCommand(ShowAdoptCalculatorDialogAsync);
DangerousLoginMihoyoBbsCommand = new AsyncRelayCommand(DangerousLoginMihoyoBbsAsync);
DownloadStaticFileCommand = new AsyncRelayCommand(DownloadStaticFileAsync);
+ HutaoDatabasePresentCommand = new RelayCommand(HutaoDatabasePresent);
}
///
@@ -49,6 +50,11 @@ internal class TestViewModel : Abstraction.ViewModel
///
public ICommand DownloadStaticFileCommand { get; }
+ ///
+ /// 胡桃数据库呈现命令
+ ///
+ public ICommand HutaoDatabasePresentCommand { get; }
+
private async Task ShowCommunityGameRecordDialogAsync()
{
// ContentDialog must be created by main thread.
@@ -115,4 +121,11 @@ internal class TestViewModel : Abstraction.ViewModel
}
}
}
+
+ private void HutaoDatabasePresent()
+ {
+ Ioc.Default
+ .GetRequiredService()
+ .Navigate(Service.Navigation.INavigationAwaiter.Default);
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
index 350d6c51..a1abdcd1 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/WelcomeViewModel.cs
@@ -65,11 +65,7 @@ internal class WelcomeViewModel : ObservableObject
})).ConfigureAwait(true);
serviceProvider.GetRequiredService().Send(new Message.WelcomeStateCompleteMessage());
-
- // Complete StaticResourceContracts
- LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
- LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
- LocalSetting.Set(SettingKeys.StaticResourceV3Contract, true);
+ StaticResource.FulfillAllContracts();
try
{
@@ -115,6 +111,11 @@ internal class WelcomeViewModel : ObservableObject
downloadSummaries.TryAdd("Talent", new(serviceProvider, "命之座图标更新", "Talent"));
}
+ if (StaticResource.IsContractUnfulfilled(SettingKeys.StaticResourceV4Contract))
+ {
+ downloadSummaries.TryAdd("AvatarIcon", new(serviceProvider, "角色图标更新", "AvatarIcon"));
+ }
+
return downloadSummaries.Select(x => x.Value);
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs
index 4ef215ec..3cb88cdc 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiEndpoints.cs
@@ -316,13 +316,11 @@ internal static class ApiEndpoints
///
/// 启动器资源
///
- /// 启动器Id
- /// 通道
- /// 子通道
+ /// 启动方案
/// 启动器资源字符串
- public static string SdkStaticLauncherResource(string launcherId, string channel, string subChannel)
+ public static string SdkStaticLauncherResource(Model.Binding.LaunchGame.LaunchScheme scheme)
{
- return $"{SdkStaticLauncherApi}/resource?key=eYd89JmJ&launcher_id={launcherId}&channel_id={channel}&sub_channel_id={subChannel}";
+ return $"{SdkStaticLauncherApi}/resource?key={scheme.Key}&launcher_id={scheme.LauncherId}&channel_id={scheme.Channel}&sub_channel_id={scheme.SubChannel}";
}
// https://sdk-static.mihoyo.com/hk4e_cn/mdk/launcher/api/content?filter_adv=true&key=eYd89JmJ&language=zh-cn&launcher_id=18
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
index cb1ab1c4..28c650f4 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/ApiOsEndpoints.cs
@@ -23,8 +23,24 @@ internal static class ApiOsEndpoints
}
#endregion
+ #region SdkStaticLauncherApi
+
+ ///
+ /// 启动器资源
+ ///
+ /// 启动方案
+ /// 启动器资源字符串
+ public static string SdkOsStaticLauncherResource(Model.Binding.LaunchGame.LaunchScheme scheme)
+ {
+ return $"{SdkOsStaticLauncherApi}/resource?key={scheme.Key}&launcher_id={scheme.LauncherId}&channel_id={scheme.Channel}&sub_channel_id={scheme.SubChannel}";
+ }
+ #endregion
+
#region Hosts | Queries
private const string Hk4eApiOs = "https://hk4e-api-os.hoyoverse.com";
private const string Hk4eApiOsGachaInfoApi = $"{Hk4eApiOs}/event/gacha_info/api";
+
+ private const string SdkOsStatic = "https://sdk-os-static.mihoyo.com";
+ private const string SdkOsStaticLauncherApi = $"{SdkOsStatic}/hk4e_global/mdk/launcher/api";
#endregion
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/GameResource.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/GameResource.cs
index b2e5a682..5cc33569 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/GameResource.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/GameResource.cs
@@ -45,10 +45,10 @@ public class GameResource
public List DeprecatedPackages { get; set; } = default!;
///
- /// 渠道服sdk
+ /// 渠道服 sdk
///
[JsonPropertyName("sdk")]
- public object? Sdk { get; set; }
+ public Sdk? Sdk { get; set; }
///
/// 过期的单个文件
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Package.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Package.cs
index 0cb9f311..c962bd72 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Package.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Package.cs
@@ -38,8 +38,14 @@ public class Package : PathMd5
[JsonPropertyName("voice_packs")]
public List VoicePacks { get; set; } = default!;
- // We don't want to support
- // decompressed_path & segments
+ ///
+ /// 松散文件
+ /// 用于校验完整性
+ ///
+ [JsonPropertyName("decompressed_path")]
+ public string DecompressedPath { get; set; } = default!;
+
+ // We don't want to support `segments` downloading
///
/// 包大小 bytes
@@ -47,4 +53,4 @@ public class Package : PathMd5
[JsonPropertyName("package_size")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long PackageSize { get; set; } = default!;
-}
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs
index 4de96a04..42313bca 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/ResourceClient.cs
@@ -39,7 +39,10 @@ internal class ResourceClient
/// 游戏资源
public async Task> GetResourceAsync(LaunchScheme scheme, CancellationToken token = default)
{
- string url = ApiEndpoints.SdkStaticLauncherResource(scheme.LauncherId, scheme.Channel, scheme.SubChannel);
+ string url = scheme.LauncherId == "10"
+ ? ApiOsEndpoints.SdkOsStaticLauncherResource(scheme)
+ : ApiEndpoints.SdkStaticLauncherResource(scheme);
+
Response? response = await httpClient
.TryCatchGetFromJsonAsync>(url, options, logger, token)
.ConfigureAwait(false);
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Sdk.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Sdk.cs
new file mode 100644
index 00000000..58739064
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/SdkStatic/Hk4e/Launcher/Sdk.cs
@@ -0,0 +1,28 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
+
+///
+/// Sdk
+///
+public class Sdk : PathMd5
+{
+ ///
+ /// 版本
+ ///
+ [JsonPropertyName("version")]
+ public string Version { get; set; } = default!;
+
+ ///
+ /// 常量 sdk_pkg_version
+ ///
+ [JsonPropertyName("pkg_version")]
+ public string PackageVersion { get; set; } = default!;
+
+ ///
+ /// 描述
+ ///
+ [JsonPropertyName("desc")]
+ public string Description { get; set; } = default!;
+}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs
index f6c91c94..1110392e 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Response/KnownReturnCode.cs
@@ -63,6 +63,11 @@ public enum KnownReturnCode
///
LoginDataOutdated = -262,
+ ///
+ /// 无效的 Key
+ ///
+ InvalidKey = -205,
+
///
/// 访问过于频繁
///
@@ -118,6 +123,11 @@ public enum KnownReturnCode
///
DataIsNotPublicForTheUser = 10102,
+ ///
+ /// 实时便笺
+ ///
+ CODE10103 = 10103,
+
///
/// 实时便笺
///