fix package convert issue

This commit is contained in:
DismissedLight
2023-01-30 10:43:05 +08:00
parent f7f2d9c867
commit bb01f3a3cb
39 changed files with 519 additions and 181 deletions

View File

@@ -7,7 +7,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 数据库集合上下文
/// 数据库集合扩展
/// </summary>
public static class DbSetExtension
{
@@ -134,4 +134,4 @@ public static class DbSetExtension
dbSet.Update(entity);
return await dbSet.Context().SaveChangesAsync().ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
namespace Snap.Hutao.Core.Database;
/// <summary>
/// 可查询扩展
/// </summary>
public static class QueryableExtension
{
/// <summary>
/// source.Where(predicate).ExecuteDeleteAsync(token)
/// </summary>
/// <typeparam name="TSource">源类型</typeparam>
/// <param name="source">源</param>
/// <param name="predicate">条件</param>
/// <param name="token">取消令牌</param>
/// <returns>SQL返回个数</returns>
public static Task<int> ExecuteDeleteWhereAsync<TSource>(this IQueryable<TSource> source, Expression<Func<TSource, bool>> predicate, CancellationToken token = default)
{
return source.Where(predicate).ExecuteDeleteAsync(token);
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using System.Security.Cryptography;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 摘要
/// </summary>
internal static class Digest
{
/// <summary>
/// 异步获取文件 Md5 摘要
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="token">取消令牌</param>
/// <returns>文件 Md5 摘要</returns>
public static async Task<string> GetFileMd5Async(string filePath, CancellationToken token = default)
{
using (FileStream stream = File.OpenRead(filePath))
{
return await GetStreamMd5Async(stream, token).ConfigureAwait(false);
}
}
/// <summary>
/// 获取流的 Md5 摘要
/// </summary>
/// <param name="stream">流</param>
/// <param name="token">取消令牌</param>
/// <returns>流 Md5 摘要</returns>
public static async Task<string> GetStreamMd5Async(Stream stream, CancellationToken token = default)
{
using (MD5 md5 = MD5.Create())
{
byte[] bytes = await md5.ComputeHashAsync(stream, token).ConfigureAwait(false);
return System.Convert.ToHexString(bytes);
}
}
}

View File

@@ -1,31 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
using System.Security.Cryptography;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 文件摘要
/// </summary>
internal static class FileDigest
{
/// <summary>
/// 异步获取文件 Md5 摘要
/// </summary>
/// <param name="filePath">文件路径</param>
/// <param name="token">取消令牌</param>
/// <returns>文件 Md5 摘要</returns>
public static async Task<string> 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);
}
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.IO;
namespace Snap.Hutao.Core.IO;
/// <summary>
/// 文件操作
/// </summary>
internal static class FileOperation
{
/// <summary>
/// 将指定文件移动到新位置,提供指定新文件名和覆盖目标文件(如果它已存在)的选项。
/// </summary>
/// <param name="sourceFileName">要移动的文件的名称。 可以包括相对或绝对路径。</param>
/// <param name="destFileName">文件的新路径和名称。</param>
/// <param name="overwrite">如果要覆盖目标文件</param>
/// <returns>是否发生了移动操作</returns>
public static bool Move(string sourceFileName, string destFileName, bool overwrite)
{
if (File.Exists(sourceFileName))
{
if (overwrite)
{
File.Move(sourceFileName, destFileName, overwrite);
return true;
}
else
{
if (!File.Exists(destFileName))
{
File.Move(sourceFileName, destFileName, overwrite);
return true;
}
}
}
return false;
}
}

View File

@@ -53,6 +53,12 @@ internal sealed class TempFile : IDisposable
/// </summary>
public void Dispose()
{
File.Delete(Path);
try
{
File.Delete(Path);
}
catch (IOException)
{
}
}
}

View File

@@ -10,7 +10,9 @@ using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Service.Navigation;
#if RELEASE
using System.Security.Principal;
#endif
namespace Snap.Hutao.Core.LifeCycle;
@@ -37,11 +39,15 @@ internal static class Activation
/// <returns>是否提升了权限</returns>
public static bool GetElevated()
{
#if RELEASE
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
WindowsPrincipal principal = new(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
#else
return true;
#endif
}
/// <summary>

View File

@@ -11,7 +11,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio&amp;comma DissmissedLight"
Publisher="CN=DGP Studio"
Version="1.4.0.0" />
<Properties>
@@ -24,7 +24,7 @@
<Dependencies>
<!--<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />-->
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.18362.0" MaxVersionTested="10.0.22621.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.22621.0" />
</Dependencies>
<Resources>

View File

@@ -72,8 +72,7 @@ internal class AchievementService : IAchievementService
// Cascade deleted the achievements.
await appDbContext.AchievementArchives
.Where(a => a.InnerId == archive.InnerId)
.ExecuteDeleteAsync()
.ExecuteDeleteWhereAsync(a => a.InnerId == archive.InnerId)
.ConfigureAwait(false);
}

View File

@@ -60,7 +60,7 @@ internal class SummaryAvatarFactory
FetterLevel = avatarInfo.FetterInfo?.ExpLevel ?? 0,
Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap),
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
LevelNumber = int.Parse(avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
LevelNumber = avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].ValueInt32 ?? 0,
// processed webinfo part
Weapon = reliquaryAndWeapon.Weapon,

View File

@@ -105,7 +105,10 @@ internal class CultivationService : ICultivationService
await ThreadHelper.SwitchToBackgroundAsync();
using (IServiceScope scope = scopeFactory.CreateScope())
{
await scope.ServiceProvider.GetRequiredService<AppDbContext>().CultivateProjects.RemoveAndSaveAsync(project).ConfigureAwait(false);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.CultivateProjects
.ExecuteDeleteWhereAsync(p => p.InnerId == project.InnerId)
.ConfigureAwait(false);
}
}

View File

@@ -157,14 +157,14 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
}
/// <inheritdoc/>
public void RemoveDailyNote(DailyNoteEntry entry)
public async Task RemoveDailyNoteAsync(DailyNoteEntry entry)
{
entries!.Remove(entry);
using (IServiceScope scope = scopeFactory.CreateScope())
{
// DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s)
scope.ServiceProvider.GetRequiredService<AppDbContext>().DailyNotes.RemoveAndSave(entry);
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.DailyNotes.ExecuteDeleteWhereAsync(d => d.InnerId == entry.InnerId).ConfigureAwait(false);
}
}
}

View File

@@ -36,5 +36,6 @@ public interface IDailyNoteService
/// 移除指定的实时便笺
/// </summary>
/// <param name="entry">指定的实时便笺</param>
void RemoveDailyNote(DailyNoteEntry entry);
/// <returns>任务</returns>
Task RemoveDailyNoteAsync(DailyNoteEntry entry);
}

View File

@@ -117,7 +117,14 @@ internal class GachaLogService : IGachaLogService
public async Task<ObservableCollection<GachaArchive>> GetArchiveCollectionAsync()
{
await ThreadHelper.SwitchToMainThreadAsync();
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
try
{
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
}
catch (SqliteException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException($"无法获取祈愿记录: {ex.Message}", ex);
}
}
/// <inheritdoc/>

View File

@@ -64,8 +64,8 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
using (MemoryStream memoryStream = new())
{
await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
string? result = Match(memoryStream, !tempFile.Path.Contains("GenshinImpact_Data"));
return new(!string.IsNullOrEmpty(result), result!);
string? result = Match(memoryStream, cacheFile.Contains(GameConstants.GenshinImpactData));
return new(!string.IsNullOrEmpty(result), result ?? "未找到可用的 Url");
}
}
}

View File

@@ -1,23 +1,6 @@
// 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;
/// <summary>
@@ -34,4 +17,4 @@ internal class GameFileOperationException : Exception
: base($"游戏文件操作失败: {message}", innerException)
{
}
}
}

View File

@@ -179,13 +179,17 @@ internal class GameService : IGameService
elements = IniSerializer.Deserialize(readStream).ToList();
}
}
catch (DirectoryNotFoundException dnfEx)
catch (FileNotFoundException ex)
{
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", dnfEx);
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", ex);
}
catch (UnauthorizedAccessException uaEx)
catch (DirectoryNotFoundException ex)
{
throw new GameFileOperationException($"无法读取或保存配置文件,请以管理员模式重试。", uaEx);
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", ex);
}
catch (UnauthorizedAccessException ex)
{
throw new GameFileOperationException($"无法读取或保存配置文件,请以管理员模式重试。", ex);
}
bool changed = false;
@@ -244,10 +248,22 @@ internal class GameService : IGameService
if (!LaunchSchemeMatchesExecutable(launchScheme, gameFileName))
{
await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false);
bool replaced = await packageConverter
.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress)
.ConfigureAwait(false);
// We need to change the gamePath if we switched.
OverwriteGamePath(Path.Combine(gameFolder, launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName));
if (replaced)
{
// We need to change the gamePath if we switched.
string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName;
OverwriteGamePath(Path.Combine(gameFolder, exeName));
}
else
{
// We can't start the game
// when we failed to convert game
return false;
}
}
if (!launchScheme.IsOversea)
@@ -364,7 +380,15 @@ internal class GameService : IGameService
string? registrySdk = GameAccountRegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
{
GameAccount? account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
GameAccount? account;
try
{
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("已存在多个匹配账号,请先删除重复的账号", ex);
}
if (account == null)
{

View File

@@ -0,0 +1,25 @@
// 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 System.IO;
using System.IO.Compression;
using System.Net.Http;
using static Snap.Hutao.Service.Game.GameConstants;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
/// 包转换异常
/// </summary>
public class PackageConvertException : Exception
{
/// <inheritdoc cref="Exception.Exception(string?, Exception?)"/>
public PackageConvertException(string message, Exception innerException)
: base(message, innerException)
{
}
}

View File

@@ -3,10 +3,8 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Response;
using System.IO;
using System.IO.Compression;
using System.Net.Http;
@@ -20,7 +18,6 @@ namespace Snap.Hutao.Service.Game.Package;
[HttpClient(HttpClientConfigration.Default)]
internal class PackageConverter
{
private readonly ResourceClient resourceClient;
private readonly JsonSerializerOptions options;
private readonly HttpClient httpClient;
@@ -30,9 +27,8 @@ internal class PackageConverter
/// <param name="resourceClient">资源客户端</param>
/// <param name="options">Json序列化选项</param>
/// <param name="httpClient">http客户端</param>
public PackageConverter(ResourceClient resourceClient, JsonSerializerOptions options, HttpClient httpClient)
public PackageConverter(JsonSerializerOptions options, HttpClient httpClient)
{
this.resourceClient = resourceClient;
this.options = options;
this.httpClient = httpClient;
}
@@ -46,18 +42,25 @@ internal class PackageConverter
/// <param name="gameFolder">游戏目录</param>
/// <param name="progress">进度</param>
/// <returns>替换结果与资源</returns>
public async Task EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResouce, string gameFolder, IProgress<PackageReplaceStatus> progress)
public async Task<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResouce, string gameFolder, IProgress<PackageReplaceStatus> progress)
{
await ThreadHelper.SwitchToBackgroundAsync();
string scatteredFilesUrl = gameResouce.Game.Latest.DecompressedPath;
Uri pkgVersionUri = new($"{scatteredFilesUrl}/pkg_version");
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
progress.Report(new("下载包版本信息"));
progress.Report(new("获取 Package Version"));
Dictionary<string, VersionItem> remoteItems;
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
try
{
remoteItems = await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false);
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
{
remoteItems = await GetVersionItemsAsync(remoteSteam).ConfigureAwait(false);
}
}
catch (IOException ex)
{
throw new PackageConvertException("下载 Package Version 失败", ex);
}
Dictionary<string, VersionItem> localItems;
@@ -67,7 +70,7 @@ internal class PackageConverter
}
IEnumerable<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems);
await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
return await ReplaceGameResourceAsync(diffOperations, gameFolder, scatteredFilesUrl, direction, progress).ConfigureAwait(false);
}
/// <summary>
@@ -100,8 +103,8 @@ internal class PackageConverter
{
if (File.Exists(sdkDllBackup) && File.Exists(sdkVersionBackup))
{
File.Move(sdkDllBackup, sdkDll, false);
File.Move(sdkVersionBackup, sdkVersion, false);
FileOperation.Move(sdkDllBackup, sdkDll, false);
FileOperation.Move(sdkVersionBackup, sdkVersion, false);
}
else
{
@@ -124,15 +127,9 @@ internal class PackageConverter
}
else
{
if (File.Exists(sdkDll))
{
File.Move(sdkDll, sdkDllBackup, true);
}
if (File.Exists(sdkVersion))
{
File.Move(sdkVersion, sdkVersionBackup, true);
}
// backup
FileOperation.Move(sdkDll, sdkDllBackup, true);
FileOperation.Move(sdkVersion, sdkVersionBackup, true);
}
}
@@ -190,11 +187,17 @@ internal class PackageConverter
// so we assume the data folder is present
if (direction == ConvertDirection.ChineseToOversea)
{
Directory.Move(yuanShenData, genshinImpactData);
if (Directory.Exists(yuanShenData))
{
Directory.Move(yuanShenData, genshinImpactData);
}
}
else
{
Directory.Move(genshinImpactData, yuanShenData);
if (Directory.Exists(genshinImpactData))
{
Directory.Move(genshinImpactData, yuanShenData);
}
}
}
@@ -204,10 +207,42 @@ internal class PackageConverter
File.Move(targetFullPath, cacheFilePath, true);
}
private async Task ReplaceGameResourceAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
private static async Task CopyToWithProgressAsync(Stream source, Stream target, string name, long totalBytes, IProgress<PackageReplaceStatus> progress)
{
const int bufferSize = 81920;
long totalBytesRead = 0;
int bytesRead;
Memory<byte> buffer = new byte[bufferSize];
do
{
bytesRead = await source.ReadAsync(buffer).ConfigureAwait(false);
await target.WriteAsync(buffer[..bytesRead]).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress.Report(new(name, totalBytesRead, totalBytes));
if (bytesRead <= 0)
{
break;
}
}
while (bytesRead > 0);
}
private async Task<bool> ReplaceGameResourceAsync(IEnumerable<ItemOperationInfo> operations, string gameFolder, string scatteredFilesUrl, ConvertDirection direction, IProgress<PackageReplaceStatus> progress)
{
// 重命名 _Data 目录
RenameDataFolder(gameFolder, direction);
try
{
RenameDataFolder(gameFolder, direction);
}
catch (IOException)
{
// Access to the path is denied.
// When user install the game in special folder like 'Program Files'
return false;
}
// Ensure cache folder
string cacheFolder = Path.Combine(gameFolder, "Screenshot", "HutaoCache");
@@ -224,13 +259,12 @@ internal class PackageConverter
switch (info.Type)
{
case ItemOperationType.Add:
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info).ConfigureAwait(false);
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
break;
case ItemOperationType.Replace:
{
MoveToCache(moveToFilePath, targetFilePath);
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info).ConfigureAwait(false);
await ReplaceFromCacheOrWebAsync(cacheFilePath, targetFilePath, scatteredFilesUrl, info, progress).ConfigureAwait(false);
break;
}
@@ -245,17 +279,18 @@ internal class PackageConverter
// 重新下载所有 *pkg_version 文件
await ReplacePackageVersionsAsync(scatteredFilesUrl, gameFolder).ConfigureAwait(false);
return true;
}
private async Task ReplaceFromCacheOrWebAsync(string cacheFilePath, string targetFilePath, string scatteredFilesUrl, ItemOperationInfo info)
private async Task ReplaceFromCacheOrWebAsync(string cacheFilePath, string targetFilePath, string scatteredFilesUrl, ItemOperationInfo info, IProgress<PackageReplaceStatus> progress)
{
if (File.Exists(cacheFilePath))
{
string remoteMd5 = await FileDigest.GetMd5Async(cacheFilePath, CancellationToken.None).ConfigureAwait(false);
string remoteMd5 = await Digest.GetFileMd5Async(cacheFilePath).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
// There shouldn't be any file in the path/name
File.Move(cacheFilePath, targetFilePath, false);
return;
}
@@ -269,9 +304,32 @@ internal class PackageConverter
// Cache no item, download it anyway.
using (FileStream fileStream = File.Create(targetFilePath))
{
using (Stream webStream = await httpClient.GetStreamAsync($"{scatteredFilesUrl}/{info.Target}").ConfigureAwait(false))
while (true)
{
await webStream.CopyToAsync(fileStream).ConfigureAwait(false);
using (HttpResponseMessage response = await httpClient.GetAsync($"{scatteredFilesUrl}/{info.Target}", HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false))
{
long totalBytes = response.Content.Headers.ContentLength ?? 0;
using (Stream webStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
try
{
await CopyToWithProgressAsync(webStream, fileStream, info.Target, totalBytes, progress).ConfigureAwait(false);
fileStream.Seek(0, SeekOrigin.Begin);
string remoteMd5 = await Digest.GetStreamMd5Async(fileStream).ConfigureAwait(false);
if (info.Md5 == remoteMd5.ToLowerInvariant())
{
return;
}
}
catch
{
// System.IO.IOException: The response ended prematurely.
// System.IO.IOException: Received an unexpected EOF or 0 bytes from the transport stream.
// We want to retry forever.
}
}
}
}
}
}
@@ -320,6 +378,7 @@ internal class PackageConverter
private async Task<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream, ConvertDirection direction, Func<string, ConvertDirection, string> nameConverter)
{
Dictionary<string, VersionItem> results = new();
using (StreamReader reader = new(stream))
{
while (await reader.ReadLineAsync().ConfigureAwait(false) is string raw)

View File

@@ -1,6 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Common;
namespace Snap.Hutao.Service.Game.Package;
/// <summary>
@@ -17,8 +19,40 @@ public class PackageReplaceStatus
Description = description;
}
/// <summary>
/// 构造一个新的包更新状态
/// </summary>
/// <param name="name">名称</param>
/// <param name="bytesRead">读取的字节数</param>
/// <param name="totalBytes">总字节数</param>
public PackageReplaceStatus(string name, long bytesRead, long totalBytes)
{
Percent = (double)bytesRead / totalBytes;
Description = $"{name}\n{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}";
}
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 是否有进度
/// </summary>
public bool IsIndeterminate { get => Percent < 0; }
/// <summary>
/// 进度
/// </summary>
public double Percent { get; set; } = -1;
/// <summary>
/// 克隆
/// </summary>
/// <returns>克隆的实例</returns>
public PackageReplaceStatus Clone()
{
// 进度需要在主线程上创建
return new(Description) { Percent = Percent };
}
}

View File

@@ -0,0 +1,28 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Win32;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
using Windows.Win32.System.Diagnostics.ToolHelp;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary>
/// 游戏帧率解锁器异常
/// </summary>
internal class GameFpsUnlockerException : Exception
{
/// <summary>
/// 构造一个新的用户数据损坏异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public GameFpsUnlockerException(Exception innerException)
: base($"解锁帧率失败: {innerException.Message}", innerException)
{
}
}

View File

@@ -135,7 +135,7 @@ internal partial class MetadataService : IMetadataService, IMetadataServiceIniti
string fileFullPath = Path.Combine(metadataFolderPath, fileFullName);
if (File.Exists(fileFullPath))
{
skip = md5 == await FileDigest.GetMd5Async(fileFullPath, token).ConfigureAwait(false);
skip = md5 == await Digest.GetFileMd5Async(fileFullPath, token).ConfigureAwait(false);
}
if (!skip)

View File

@@ -76,7 +76,14 @@ internal class UserService : IUserService
if (currentUser != null)
{
currentUser.IsSelected = true;
appDbContext.Users.UpdateAndSave(currentUser.Entity);
try
{
appDbContext.Users.UpdateAndSave(currentUser.Entity);
}
catch (InvalidOperationException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException($"用户 {currentUser.UserInfo?.Uid} 状态保存失败", ex);
}
}
messenger.Send(message);

View File

@@ -22,7 +22,6 @@
</ContentDialog.Resources>
<StackPanel>
<TextBlock Text="祈愿记录Url已失效请重新获取" Visibility="{x:Bind State.AuthKeyTimeout, Converter={StaticResource BoolToVisibilityConverter}, Mode=OneWay}"/>
<cwucont:HeaderedItemsControl
x:Name="GachaItemsPresenter"
Padding="0,8,0,0"

View File

@@ -45,12 +45,14 @@ public sealed partial class GachaLogRefreshProgressDialog : ContentDialog
{
State = state;
GachaItemsPresenter.Header = state.AuthKeyTimeout
? null
? "祈愿记录Url已失效请重新获取"
: (object)$"正在获取 {state.ConfigType.GetDescription()}";
// Binding not working here.
GachaItemsPresenter.Items.Clear();
foreach (ItemBase item in state.Items)
// System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
foreach (ItemBase item in state.Items.ToList())
{
GachaItemsPresenter.Items.Add(new ItemIcon
{

View File

@@ -17,9 +17,12 @@
<TextBlock Text="转换可能需要花费一段时间,请勿关闭胡桃"/>
<TextBlock
MinWidth="360"
Margin="0,8,0,16"
Margin="0,16,0,8"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding Description}"/>
<ProgressBar IsIndeterminate="True"/>
Text="{Binding State.Description}"/>
<ProgressBar
IsIndeterminate="{Binding State.IsIndeterminate}"
Maximum="1"
Value="{Binding State.Percent}"/>
</StackPanel>
</ContentDialog>

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control;
using Snap.Hutao.Service.Game.Package;
namespace Snap.Hutao.View.Dialog;
@@ -12,7 +13,7 @@ namespace Snap.Hutao.View.Dialog;
/// </summary>
public sealed partial class LaunchGamePackageConvertDialog : ContentDialog
{
private static readonly DependencyProperty DescriptionProperty = Property<LaunchGamePackageConvertDialog>.Depend(nameof(Description), "请稍候");
private static readonly DependencyProperty StateProperty = Property<LaunchGamePackageConvertDialog>.Depend<PackageReplaceStatus>(nameof(State));
/// <summary>
/// 构造一个新的启动游戏客户端转换对话框
@@ -27,9 +28,9 @@ public sealed partial class LaunchGamePackageConvertDialog : ContentDialog
/// <summary>
/// 描述
/// </summary>
public string Description
public PackageReplaceStatus State
{
get { return (string)GetValue(DescriptionProperty); }
set { SetValue(DescriptionProperty, value); }
get { return (PackageReplaceStatus)GetValue(StateProperty); }
set { SetValue(StateProperty, value); }
}
}

View File

@@ -81,48 +81,6 @@
<PivotItem Header="材料清单">
<Grid>
<Pivot Visibility="{Binding CultivateEntries.Count, Converter={StaticResource Int32ToVisibilityConverter}}">
<PivotItem Header="材料统计">
<cwucont:AdaptiveGridView
Padding="16,16,4,4"
cwua:ItemsReorderAnimation.Duration="0:0:0.1"
DesiredWidth="320"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
ItemsSource="{Binding StatisticsItems}"
SelectionMode="None">
<cwucont:AdaptiveGridView.Resources>
<x:Double x:Key="GridViewItemMinHeight">0</x:Double>
</cwucont:AdaptiveGridView.Resources>
<cwucont:AdaptiveGridView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<shvc:ItemIcon
Grid.Column="0"
Width="32"
Height="32"
Icon="{Binding Inner.Icon, Converter={StaticResource ItemIconConverter}}"
Quality="{Binding Inner.RankLevel}"/>
<TextBlock
Grid.Column="1"
Margin="16,0,0,0"
VerticalAlignment="Center"
Text="{Binding Inner.Name}"
TextTrimming="CharacterEllipsis"/>
<TextBlock
Grid.Column="2"
Margin="16,0,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{Binding CountFormatted}"/>
</Grid>
</DataTemplate>
</cwucont:AdaptiveGridView.ItemTemplate>
</cwucont:AdaptiveGridView>
</PivotItem>
<PivotItem Header="养成物品">
<cwucont:AdaptiveGridView
Padding="16,16,4,4"
@@ -273,6 +231,48 @@
</cwucont:AdaptiveGridView.ItemTemplate>
</cwucont:AdaptiveGridView>
</PivotItem>
<PivotItem Header="材料统计">
<cwucont:AdaptiveGridView
Padding="16,16,4,4"
cwua:ItemsReorderAnimation.Duration="0:0:0.1"
DesiredWidth="320"
ItemContainerStyle="{StaticResource LargeGridViewItemStyle}"
ItemsSource="{Binding StatisticsItems}"
SelectionMode="None">
<cwucont:AdaptiveGridView.Resources>
<x:Double x:Key="GridViewItemMinHeight">0</x:Double>
</cwucont:AdaptiveGridView.Resources>
<cwucont:AdaptiveGridView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
<ColumnDefinition/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<shvc:ItemIcon
Grid.Column="0"
Width="32"
Height="32"
Icon="{Binding Inner.Icon, Converter={StaticResource ItemIconConverter}}"
Quality="{Binding Inner.RankLevel}"/>
<TextBlock
Grid.Column="1"
Margin="16,0,0,0"
VerticalAlignment="Center"
Text="{Binding Inner.Name}"
TextTrimming="CharacterEllipsis"/>
<TextBlock
Grid.Column="2"
Margin="16,0,4,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Text="{Binding CountFormatted}"/>
</Grid>
</DataTemplate>
</cwucont:AdaptiveGridView.ItemTemplate>
</cwucont:AdaptiveGridView>
</PivotItem>
</Pivot>
<StackPanel
HorizontalAlignment="Center"

View File

@@ -62,8 +62,8 @@
<InfoBar
IsClosable="False"
IsOpen="{Binding IsElevated}"
Message="切换国际服功能尚处于测试阶段,可能出现未预料的问题,请注意备份游戏资源!"
Severity="Error"/>
Message="切换国际服功能会在游戏截图文件创建缓存文件夹"
Severity="Informational"/>
<wsc:Setting
Description="切换游戏服务器(国服/渠道服/国际服)"
Header="服务器"

View File

@@ -82,6 +82,13 @@ public sealed partial class LoginMihoyoUserPage : Microsoft.UI.Xaml.Controls.Pag
switch (result)
{
case UserOptionResult.Added:
ViewModel.UserViewModel vm = Ioc.Default.GetRequiredService<ViewModel.UserViewModel>();
if (vm.Users!.Count == 1)
{
await ThreadHelper.SwitchToMainThreadAsync();
vm.SelectedUser = vm.Users.Single();
}
infoBarService.Success($"用户 [{nickname}] 添加成功");
break;
case UserOptionResult.Incomplete:

View File

@@ -313,11 +313,22 @@ internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipien
if (result == ContentDialogResult.Primary)
{
await achievementService.RemoveArchiveAsync(SelectedArchive).ConfigureAwait(false);
try
{
ThrowIfViewDisposed();
using (await DisposeLock.EnterAsync(CancellationToken).ConfigureAwait(false))
{
ThrowIfViewDisposed();
await achievementService.RemoveArchiveAsync(SelectedArchive).ConfigureAwait(false);
}
// Re-select first archive
await ThreadHelper.SwitchToMainThreadAsync();
SelectedArchive = Archives.FirstOrDefault();
// Re-select first archive
await ThreadHelper.SwitchToMainThreadAsync();
SelectedArchive = Archives.FirstOrDefault();
}
catch (OperationCanceledException)
{
}
}
}
}

View File

@@ -63,7 +63,7 @@ internal class DailyNoteViewModel : Abstraction.ViewModel
OpenUICommand = new AsyncRelayCommand(OpenUIAsync);
TrackRoleCommand = new AsyncRelayCommand<UserAndUid>(TrackRoleAsync);
RefreshCommand = new AsyncRelayCommand(RefreshAsync);
RemoveDailyNoteCommand = new RelayCommand<DailyNoteEntry>(RemoveDailyNote);
RemoveDailyNoteCommand = new AsyncRelayCommand<DailyNoteEntry>(RemoveDailyNoteAsync);
ModifyNotificationCommand = new AsyncRelayCommand<DailyNoteEntry>(ModifyDailyNoteNotificationAsync);
DailyNoteVerificationCommand = new AsyncRelayCommand(VerifyDailyNoteVerificationAsync);
}
@@ -229,11 +229,11 @@ internal class DailyNoteViewModel : Abstraction.ViewModel
await dailyNoteService.RefreshDailyNotesAsync(false).ConfigureAwait(false);
}
private void RemoveDailyNote(DailyNoteEntry? entry)
private async Task RemoveDailyNoteAsync(DailyNoteEntry? entry)
{
if (entry != null)
{
dailyNoteService.RemoveDailyNote(entry);
await dailyNoteService.RemoveDailyNoteAsync(entry).ConfigureAwait(false);
}
}

View File

@@ -12,6 +12,7 @@ using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Game.Unlocker;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
@@ -305,10 +306,13 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
// access level is already high enough.
await ThreadHelper.SwitchToMainThreadAsync();
LaunchGamePackageConvertDialog dialog = new();
Progress<Service.Game.Package.PackageReplaceStatus> progress = new(state => dialog.State = state.Clone());
await using (await dialog.BlockAsync().ConfigureAwait(false))
{
Progress<Service.Game.Package.PackageReplaceStatus> progress = new(s => dialog.Description = s.Description);
await gameService.EnsureGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false);
if (!await gameService.EnsureGameResourceAsync(SelectedScheme, progress).ConfigureAwait(false))
{
infoBarService.Warning("切换服务器失败");
}
}
}
@@ -325,7 +329,7 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
LaunchConfiguration configuration = new(IsExclusive, IsFullScreen, IsBorderless, ScreenWidth, ScreenHeight, IsElevated && UnlockFps, TargetFps);
await gameService.LaunchAsync(configuration).ConfigureAwait(false);
}
catch (GameFileOperationException ex)
catch (Exception ex)
{
infoBarService.Error(ex);
}
@@ -334,7 +338,14 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
private async Task DetectGameAccountAsync()
{
await gameService.DetectGameAccountAsync().ConfigureAwait(false);
try
{
await gameService.DetectGameAccountAsync().ConfigureAwait(false);
}
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
Ioc.Default.GetRequiredService<IInfoBarService>().Error(ex);
}
}
private void AttachGameAccountToCurrentUserGameRole(GameAccount? gameAccount)

View File

@@ -57,11 +57,11 @@ internal class WelcomeViewModel : ObservableObject
// Cancel all previous created jobs
serviceProvider.GetRequiredService<BitsManager>().CancelAllJobs();
await Task.WhenAll(downloadSummaries.Select(async d =>
await Task.WhenAll(downloadSummaries.Select(async downloadTask =>
{
await d.DownloadAndExtractAsync().ConfigureAwait(false);
await downloadTask.DownloadAndExtractAsync().ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
DownloadSummaries.Remove(d);
DownloadSummaries.Remove(downloadTask);
})).ConfigureAwait(true);
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());

View File

@@ -19,6 +19,7 @@ using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Response;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using CalcAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalcClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
using CalcConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
@@ -178,7 +179,13 @@ internal class WikiAvatarViewModel : Abstraction.ViewModel
if (!Avatars.Contains(Selected))
{
Avatars.MoveCurrentToFirst();
try
{
Avatars.MoveCurrentToFirst();
}
catch (COMException)
{
}
}
}
else

View File

@@ -18,6 +18,7 @@ using Snap.Hutao.Service.User;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Response;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using CalcAvatarPromotionDelta = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.AvatarPromotionDelta;
using CalcClient = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.CalculateClient;
using CalcConsumption = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Consumption;
@@ -176,7 +177,13 @@ internal class WikiWeaponViewModel : Abstraction.ViewModel
if (!Weapons.Contains(Selected))
{
Weapons.MoveCurrentToFirst();
try
{
Weapons.MoveCurrentToFirst();
}
catch (COMException)
{
}
}
}
else

View File

@@ -14,6 +14,7 @@ using Snap.Hutao.Web.Hoyolab.Bbs.User;
using Snap.Hutao.Web.Hoyolab.DynamicSecret;
using Snap.Hutao.Web.Hoyolab.Passport;
using Snap.Hutao.Web.Hoyolab.Takumi.Auth;
using System.Runtime.InteropServices;
using System.Text;
namespace Snap.Hutao.Web.Bridge;
@@ -338,7 +339,16 @@ public class MiHoYoJSInterface
logger?.LogInformation("[ExecuteScript: {callback}]\n{payload}", callback, payload);
await ThreadHelper.SwitchToMainThreadAsync();
return await webView.ExecuteScriptAsync(js);
try
{
return await webView.ExecuteScriptAsync(js);
}
catch (COMException)
{
// COMException (0x8007139F): 组或资源的状态不是执行请求操作的正确状态。 (0x8007139F)
// webview is disposing or disposed
return string.Empty;
}
}
[SuppressMessage("", "VSTHRD100")]

View File

@@ -21,4 +21,17 @@ public class TypeValue
/// </summary>
[JsonPropertyName("val")]
public string? Value { get; set; }
/// <summary>
/// 值 Int32
/// </summary>
[JsonIgnore]
public int ValueInt32
{
get
{
_ = int.TryParse(Value, out int result);
return result;
}
}
}

View File

@@ -59,8 +59,15 @@ internal class GeetestClient
/// </summary>
/// <param name="registration">验证注册</param>
/// <returns>验证方式</returns>
public Task<GeetestResult<GeetestData>?> GetAjaxAsync(VerificationRegistration registration)
public async Task<GeetestResult<GeetestData>?> GetAjaxAsync(VerificationRegistration registration)
{
return GetAjaxAsync(registration.Gt, registration.Challenge);
try
{
return await GetAjaxAsync(registration.Gt, registration.Challenge).ConfigureAwait(false);
}
catch (HttpRequestException)
{
return null;
}
}
}