bug fixes

This commit is contained in:
DismissedLight
2022-11-04 17:45:17 +08:00
parent 9e344f56e0
commit f2d63e69ea
58 changed files with 344 additions and 670 deletions

View File

@@ -9,7 +9,6 @@ using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Extension;
using Snap.Hutao.Service.AppCenter;
using Snap.Hutao.Service.Metadata;
using System.Diagnostics;
using Windows.Storage;
@@ -29,13 +28,13 @@ public partial class App : Application
/// </summary>
/// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public App(ILogger<App> logger, AppCenter appCenter)
public App(ILogger<App> logger)
{
// load app resource
InitializeComponent();
this.logger = logger;
_ = new ExceptionRecorder(this, logger, appCenter);
_ = new ExceptionRecorder(this, logger);
}
/// <inheritdoc/>
@@ -61,10 +60,6 @@ public partial class App : Application
.ImplictAs<IMetadataInitializer>()?
.InitializeInternalAsync()
.SafeForget(logger);
Ioc.Default
.GetRequiredService<AppCenter>()
.Initialize();
}
else
{

View File

@@ -18,5 +18,5 @@ internal interface ISupportAsyncInitialization
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>初始化任务</returns>
ValueTask<bool> InitializeAsync(CancellationToken token = default);
ValueTask<bool> InitializeAsync();
}

View File

@@ -53,9 +53,9 @@ internal static class CoreEnvironment
public static readonly string HoyolabDeviceId;
/// <summary>
/// AppCenter 设备Id
/// 胡桃设备Id
/// </summary>
public static readonly string AppCenterDeviceId;
public static readonly string HutaoDeviceId;
/// <summary>
/// 默认的Json序列化选项
@@ -78,7 +78,7 @@ internal static class CoreEnvironment
// simply assign a random guid
HoyolabDeviceId = Guid.NewGuid().ToString();
AppCenterDeviceId = GetUniqueUserID();
HutaoDeviceId = GetUniqueUserID();
}
private static string GetUniqueUserID()

View File

@@ -3,7 +3,7 @@
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Service.AppCenter;
using Snap.Hutao.Web.Hutao;
namespace Snap.Hutao.Core.Exception;
@@ -13,26 +13,24 @@ namespace Snap.Hutao.Core.Exception;
internal class ExceptionRecorder
{
private readonly ILogger logger;
private readonly AppCenter appCenter;
/// <summary>
/// 构造一个新的异常记录器
/// </summary>
/// <param name="application">应用程序</param>
/// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public ExceptionRecorder(Application application, ILogger logger, AppCenter appCenter)
public ExceptionRecorder(Application application, ILogger logger)
{
this.logger = logger;
this.appCenter = appCenter;
application.UnhandledException += OnAppUnhandledException;
application.DebugSettings.BindingFailed += OnXamlBindingFailed;
}
[SuppressMessage("", "VSTHRD002")]
private void OnAppUnhandledException(object? sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
appCenter.TrackCrash(e.Exception);
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(e.Exception).GetAwaiter().GetResult();
logger.LogError(EventIds.UnhandledException, e.Exception, "未经处理的异常");
foreach (ILoggerProvider provider in Ioc.Default.GetRequiredService<IEnumerable<ILoggerProvider>>())

View File

@@ -0,0 +1,46 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// Holds the task for a cancellation token, as well as the token registration. The registration is disposed when this instance is disposed.
/// </summary>
/// <typeparam name="T">包装类型</typeparam>
public sealed class CancellationTokenTaskCompletionSource : IDisposable
{
/// <summary>
/// The cancellation token registration, if any. This is <c>null</c> if the registration was not necessary.
/// </summary>
private readonly IDisposable? registration;
/// <summary>
/// Creates a task for the specified cancellation token, registering with the token if necessary.
/// </summary>
/// <param name="cancellationToken">The cancellation token to observe.</param>
public CancellationTokenTaskCompletionSource(CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
Task = Task.CompletedTask;
return;
}
var tcs = new TaskCompletionSource();
registration = cancellationToken.Register(() => tcs.TrySetResult(), useSynchronizationContext: false);
Task = tcs.Task;
}
/// <summary>
/// Gets the task for the source cancellation token.
/// </summary>
public Task Task { get; private set; }
/// <summary>
/// Disposes the cancellation token registration, if any. Note that this may cause <see cref="Task"/> to never complete.
/// </summary>
public void Dispose()
{
registration?.Dispose();
}
}

View File

@@ -28,4 +28,4 @@ internal class ConcurrentCancellationTokenSource<TItem>
return waitingItems.GetOrAdd(item, new CancellationTokenSource()).Token;
}
}
}

View File

@@ -14,8 +14,8 @@ namespace Snap.Hutao.Core;
/// </summary>
internal abstract class WebView2Helper
{
private static bool hasEverDetected = false;
private static bool isSupported = false;
private static bool hasEverDetected;
private static bool isSupported;
private static string version = "未检测到 WebView2 运行时";
/// <summary>

View File

@@ -101,7 +101,7 @@ public class SystemBackdrop
private class DispatcherQueueHelper
{
private object? dispatcherQueueController = null;
private object? dispatcherQueueController;
/// <summary>
/// 确保系统调度队列控制器存在

View File

@@ -4,7 +4,7 @@
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Factory.Abstraction;
using Snap.Hutao.Service.AppCenter;
using Snap.Hutao.Web.Hutao;
namespace Snap.Hutao.Factory;
@@ -13,17 +13,14 @@ namespace Snap.Hutao.Factory;
internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
private readonly ILogger<AsyncRelayCommandFactory> logger;
private readonly AppCenter appCenter;
/// <summary>
/// 构造一个新的异步命令工厂
/// </summary>
/// <param name="logger">日志器</param>
/// <param name="appCenter">App Center</param>
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger, AppCenter appCenter)
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger)
{
this.logger = logger;
this.appCenter = appCenter;
}
/// <inheritdoc/>
@@ -86,6 +83,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
return command;
}
[SuppressMessage("", "VSTHRD002")]
private void ReportException(IAsyncRelayCommand command)
{
command.PropertyChanged += (sender, args) =>
@@ -98,7 +96,7 @@ internal class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
Exception baseException = exception.GetBaseException();
logger.LogError(EventIds.AsyncCommandException, baseException, "{name} Exception", nameof(AsyncRelayCommand));
appCenter.TrackError(exception);
Ioc.Default.GetRequiredService<HomaClient2>().UploadLogAsync(baseException).GetAwaiter().GetResult();
}
}
}

View File

@@ -8,12 +8,6 @@ namespace Snap.Hutao.Model.Binding.AvatarProperty;
/// </summary>
public class Reliquary : EquipBase
{
/// <summary>
/// 副属性列表
/// </summary>
[Obsolete]
public List<ReliquarySubProperty> SubProperties { get; set; } = default!;
/// <summary>
/// 初始词条
/// </summary>

View File

@@ -31,6 +31,6 @@ public class UIAF
/// <returns>当前UIAF对象是否受支持</returns>
public bool IsCurrentVersionSupported()
{
return SupportedVersion.Contains(Info.UIAFVersion ?? string.Empty);
return SupportedVersion.Contains(Info?.UIAFVersion ?? string.Empty);
}
}

View File

@@ -14,7 +14,9 @@ public static class AvatarIds
public static readonly AvatarId Ayaka = 10000002;
public static readonly AvatarId Qin = 10000003;
public static readonly AvatarId PlayerBoy = 10000005;
public static readonly AvatarId Lisa = 10000006;
public static readonly AvatarId PlayerGirl = 10000007;
public static readonly AvatarId Barbara = 10000014;
public static readonly AvatarId Kaeya = 10000015;
@@ -75,4 +77,16 @@ public static class AvatarIds
public static readonly AvatarId Candace = 10000072;
public static readonly AvatarId Nahida = 10000073;
public static readonly AvatarId Layla = 10000074;
public static readonly AvatarId Wanderer = 10000075;
public static readonly AvatarId Faruzan = 10000076;
/// <summary>
/// 检查该角色是否为主角
/// </summary>
/// <param name="avatarId">角色Id</param>
/// <returns>角色是否为主角</returns>
public static bool IsPlayer(AvatarId avatarId)
{
return avatarId == PlayerBoy || avatarId == PlayerGirl;
}
}

View File

@@ -10,7 +10,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.1.18.0" />
Version="1.1.20.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -1,95 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Service.AppCenter.Model;
using Snap.Hutao.Service.AppCenter.Model.Log;
using Snap.Hutao.Web.Hoyolab;
using System.Net.Http;
namespace Snap.Hutao.Service.AppCenter;
[SuppressMessage("", "SA1600")]
[Injection(InjectAs.Singleton)]
public sealed class AppCenter : IDisposable
{
private const string AppSecret = "de5bfc48-17fc-47ee-8e7e-dee7dc59d554";
private const string API = "https://in.appcenter.ms/logs?api-version=1.0.0";
private readonly TaskCompletionSource uploadTaskCompletionSource = new();
private readonly CancellationTokenSource uploadTaskCancllationTokenSource = new();
private readonly HttpClient httpClient;
private readonly List<Log> queue;
private readonly Device deviceInfo;
private readonly JsonSerializerOptions options;
private Guid sessionID;
public AppCenter()
{
options = new(CoreEnvironment.JsonOptions);
options.Converters.Add(new LogConverter());
httpClient = new() { DefaultRequestHeaders = { { "Install-ID", CoreEnvironment.AppCenterDeviceId }, { "App-Secret", AppSecret } } };
queue = new List<Log>();
deviceInfo = new Device();
Task.Run(async () =>
{
while (!uploadTaskCancllationTokenSource.Token.IsCancellationRequested)
{
await UploadAsync().ConfigureAwait(false);
await Task.Delay(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
}
uploadTaskCompletionSource.TrySetResult();
}).SafeForget();
}
public async Task UploadAsync()
{
if (queue.Count == 0)
{
return;
}
string? uploadStatus = null;
do
{
queue.ForEach(log => log.Status = LogStatus.Uploading);
LogContainer container = new(queue);
LogUploadResult? response = await httpClient
.TryCatchPostAsJsonAsync<LogContainer, LogUploadResult>(API, container, options)
.ConfigureAwait(false);
uploadStatus = response?.Status;
}
while (uploadStatus != "Success");
queue.RemoveAll(log => log.Status == LogStatus.Uploading);
}
public void Initialize()
{
sessionID = Guid.NewGuid();
queue.Add(new StartServiceLog("Analytics", "Crashes").Initialize(sessionID, deviceInfo));
queue.Add(new StartSessionLog().Initialize(sessionID, deviceInfo).Initialize(sessionID, deviceInfo));
}
public void TrackCrash(Exception exception, bool isFatal = true)
{
queue.Add(new ManagedErrorLog(exception, isFatal).Initialize(sessionID, deviceInfo));
}
public void TrackError(Exception exception)
{
queue.Add(new HandledErrorLog(exception).Initialize(sessionID, deviceInfo));
}
[SuppressMessage("", "VSTHRD002")]
public void Dispose()
{
uploadTaskCancllationTokenSource.Cancel();
uploadTaskCompletionSource.Task.GetAwaiter().GetResult();
}
}

View File

@@ -1,64 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Windowing;
using Microsoft.Win32;
using Windows.Graphics;
namespace Snap.Hutao.Service.AppCenter;
/// <summary>
/// 设备帮助类
/// </summary>
[SuppressMessage("", "SA1600")]
public static class DeviceHelper
{
private static readonly RegistryKey? BiosKey = Registry.LocalMachine.OpenSubKey("HARDWARE\\DESCRIPTION\\System\\BIOS");
private static readonly RegistryKey? GeoKey = Registry.CurrentUser.OpenSubKey("Control Panel\\International\\Geo");
private static readonly RegistryKey? CurrentVersionKey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
public static string? GetOem()
{
string? oem = BiosKey?.GetValue("SystemManufacturer") as string;
return oem == "System manufacturer" ? null : oem;
}
public static string? GetModel()
{
string? model = BiosKey?.GetValue("SystemProductName") as string;
return model == "System Product Name" ? null : model;
}
public static string GetScreenSize()
{
RectInt32 screen = DisplayArea.Primary.OuterBounds;
return $"{screen.Width}x{screen.Height}";
}
public static string? GetCountry()
{
return GeoKey?.GetValue("Name") as string;
}
public static string GetSystemVersion()
{
object? majorVersion = CurrentVersionKey?.GetValue("CurrentMajorVersionNumber");
if (majorVersion != null)
{
object? minorVersion = CurrentVersionKey?.GetValue("CurrentMinorVersionNumber", "0");
object? buildNumber = CurrentVersionKey?.GetValue("CurrentBuildNumber", "0");
return $"{majorVersion}.{minorVersion}.{buildNumber}";
}
else
{
object? version = CurrentVersionKey?.GetValue("CurrentVersion", "0.0");
object? buildNumber = CurrentVersionKey?.GetValue("CurrentBuild", "0");
return $"{version}.{buildNumber}";
}
}
public static int GetSystemBuild()
{
return (int)(CurrentVersionKey?.GetValue("UBR") ?? 0);
}
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model;
[SuppressMessage("", "SA1600")]
public class AppCenterException
{
[JsonPropertyName("type")]
public string Type { get; set; } = "UnknownType";
[JsonPropertyName("message")]
public string? Message { get; set; }
[JsonPropertyName("stackTrace")]
public string? StackTrace { get; set; }
[JsonPropertyName("innerExceptions")]
public List<AppCenterException>? InnerExceptions { get; set; }
}

View File

@@ -1,53 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using System.Globalization;
namespace Snap.Hutao.Service.AppCenter.Model;
[SuppressMessage("", "SA1600")]
public class Device
{
[JsonPropertyName("sdkName")]
public string SdkName { get; set; } = "appcenter.winui";
[JsonPropertyName("sdkVersion")]
public string SdkVersion { get; set; } = "4.5.0";
[JsonPropertyName("osName")]
public string OsName { get; set; } = "WINDOWS";
[JsonPropertyName("osVersion")]
public string OsVersion { get; set; } = DeviceHelper.GetSystemVersion();
[JsonPropertyName("osBuild")]
public string OsBuild { get; set; } = $"{DeviceHelper.GetSystemVersion()}.{DeviceHelper.GetSystemBuild()}";
[JsonPropertyName("model")]
public string? Model { get; set; } = DeviceHelper.GetModel();
[JsonPropertyName("oemName")]
public string? OemName { get; set; } = DeviceHelper.GetOem();
[JsonPropertyName("screenSize")]
public string ScreenSize { get; set; } = DeviceHelper.GetScreenSize();
[JsonPropertyName("carrierCountry")]
public string Country { get; set; } = DeviceHelper.GetCountry() ?? "CN";
[JsonPropertyName("locale")]
public string Locale { get; set; } = CultureInfo.CurrentCulture.Name;
[JsonPropertyName("timeZoneOffset")]
public int TimeZoneOffset { get; set; } = (int)TimeZoneInfo.Local.BaseUtcOffset.TotalMinutes;
[JsonPropertyName("appVersion")]
public string AppVersion { get; set; } = CoreEnvironment.Version.ToString();
[JsonPropertyName("appBuild")]
public string AppBuild { get; set; } = CoreEnvironment.Version.ToString();
[JsonPropertyName("appNamespace")]
public string AppNamespace { get; set; } = typeof(App).Namespace ?? string.Empty;
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class EventLog : PropertiesLog
{
public EventLog(string name)
{
Name = name;
}
[JsonPropertyName("type")]
public override string Type { get => "event"; }
[JsonPropertyName("id")]
public Guid Id { get; set; } = Guid.NewGuid();
[JsonPropertyName("name")]
public string Name { get; set; }
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class HandledErrorLog : PropertiesLog
{
public HandledErrorLog(Exception exception)
{
Id = Guid.NewGuid();
Exception = LogHelper.Create(exception);
}
[JsonPropertyName("id")]
public Guid? Id { get; set; }
[JsonPropertyName("exception")]
public AppCenterException Exception { get; set; }
[JsonPropertyName("type")]
public override string Type { get => "handledError"; }
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public abstract class Log
{
[JsonIgnore]
public LogStatus Status { get; set; } = LogStatus.Pending;
[JsonPropertyName("type")]
public abstract string Type { get; }
[JsonPropertyName("sid")]
public Guid Session { get; set; }
[JsonPropertyName("timestamp")]
public string Timestamp { get; set; } = DateTime.UtcNow.ToString("yyyy'-'MM'-'dd'T'HH':'mm':'ssZ");
[JsonPropertyName("device")]
public Device Device { get; set; } = default!;
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class LogContainer
{
public LogContainer(IEnumerable<Log> logs)
{
Logs = logs;
}
[JsonPropertyName("logs")]
public IEnumerable<Log> Logs { get; set; }
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
/// <summary>
/// 日志转换器
/// </summary>
public class LogConverter : JsonConverter<Log>
{
/// <inheritdoc/>
public override Log? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
throw Must.NeverHappen();
}
/// <inheritdoc/>
public override void Write(Utf8JsonWriter writer, Log value, JsonSerializerOptions options)
{
JsonSerializer.Serialize(writer, value, value.GetType(), options);
}
}

View File

@@ -1,56 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public static class LogHelper
{
public static T Initialize<T>(this T log, Guid sid, Device device)
where T : Log
{
log.Session = sid;
log.Device = device;
return log;
}
public static AppCenterException Create(Exception exception)
{
AppCenterException current = new()
{
Type = exception.GetType().ToString(),
Message = exception.Message,
StackTrace = exception.StackTrace,
};
if (exception is AggregateException aggregateException)
{
if (aggregateException.InnerExceptions.Count != 0)
{
current.InnerExceptions = new();
foreach (var innerException in aggregateException.InnerExceptions)
{
current.InnerExceptions.Add(Create(innerException));
}
}
}
if (exception.InnerException != null)
{
current.InnerExceptions ??= new();
current.InnerExceptions.Add(Create(exception.InnerException));
}
StackTrace stackTrace = new(exception, true);
StackFrame[] frames = stackTrace.GetFrames();
if (frames.Length > 0 && frames[0].HasNativeImage())
{
}
return current;
}
}

View File

@@ -1,13 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
[SuppressMessage("", "SA1602")]
public enum LogStatus
{
Pending,
Uploading,
Uploaded,
}

View File

@@ -1,51 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core;
using System.Diagnostics;
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class ManagedErrorLog : Log
{
public ManagedErrorLog(Exception exception, bool fatal = true)
{
var p = Process.GetCurrentProcess();
Id = Guid.NewGuid();
Fatal = fatal;
UserId = CoreEnvironment.AppCenterDeviceId;
ProcessId = p.Id;
Exception = LogHelper.Create(exception);
ProcessName = p.ProcessName;
Architecture = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE");
AppLaunchTimestamp = p.StartTime.ToUniversalTime();
}
[JsonPropertyName("id")]
public Guid Id { get; set; }
[JsonPropertyName("userId")]
public string? UserId { get; set; }
[JsonPropertyName("processId")]
public int ProcessId { get; set; }
[JsonPropertyName("processName")]
public string ProcessName { get; set; }
[JsonPropertyName("fatal")]
public bool Fatal { get; set; }
[JsonPropertyName("appLaunchTimestamp")]
public DateTime? AppLaunchTimestamp { get; set; }
[JsonPropertyName("architecture")]
public string? Architecture { get; set; }
[JsonPropertyName("exception")]
public AppCenterException Exception { get; set; }
[JsonPropertyName("type")]
public override string Type { get => "managedError"; }
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class PageLog : PropertiesLog
{
public PageLog(string name)
{
Name = name;
}
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("type")]
public override string Type { get => "page"; }
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public abstract class PropertiesLog : Log
{
[JsonPropertyName("properties")]
public IDictionary<string, string> Properties { get; set; } = new Dictionary<string, string>();
}

View File

@@ -1,19 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class StartServiceLog : Log
{
public StartServiceLog(params string[] services)
{
Services = services;
}
[JsonPropertyName("services")]
public string[] Services { get; set; }
[JsonPropertyName("type")]
public override string Type { get => "startService"; }
}

View File

@@ -1,11 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model.Log;
[SuppressMessage("", "SA1600")]
public class StartSessionLog : Log
{
[JsonPropertyName("type")]
public override string Type { get => "startSession"; }
}

View File

@@ -1,20 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Service.AppCenter.Model;
[SuppressMessage("", "SA1600")]
public class LogUploadResult
{
[JsonPropertyName("status")]
public string Status { get; set; } = null!;
[JsonPropertyName("validDiagnosticsIds")]
public List<Guid> ValidDiagnosticsIds { get; set; } = null!;
[JsonPropertyName("throttledDiagnosticsIds")]
public List<Guid> ThrottledDiagnosticsIds { get; set; } = null!;
[JsonPropertyName("correlationId")]
public Guid CorrelationId { get; set; }
}

View File

@@ -2,10 +2,12 @@
// Licensed under the MIT license.
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Threading;
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Service.AvatarInfo.Factory;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Enka;
@@ -51,11 +53,14 @@ internal class AvatarInfoService : IAvatarInfoService
/// <inheritdoc/>
public async Task<ValueResult<RefreshResult, Summary?>> GetSummaryAsync(PlayerUid uid, RefreshOption refreshOption, CancellationToken token = default)
{
if (await metadataService.InitializeAsync(token).ConfigureAwait(false))
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
token.ThrowIfCancellationRequested();
if (HasOption(refreshOption, RefreshOption.RequestFromAPI))
{
EnkaResponse? resp = await GetEnkaResponseAsync(uid, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
if (resp == null)
{
return new(RefreshResult.APIUnavailable, null);
@@ -67,7 +72,8 @@ internal class AvatarInfoService : IAvatarInfoService
? UpdateDbAvatarInfo(uid.Value, resp.AvatarInfoList)
: resp.AvatarInfoList;
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(resp.PlayerInfo, list, token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
return new(RefreshResult.Ok, summary);
}
else
@@ -79,7 +85,8 @@ internal class AvatarInfoService : IAvatarInfoService
{
PlayerInfo info = PlayerInfo.CreateEmpty(uid.Value);
Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(uid.Value)).ConfigureAwait(false);
Summary summary = await GetSummaryCoreAsync(info, GetDbAvatarInfos(uid.Value), token).ConfigureAwait(false);
token.ThrowIfCancellationRequested();
return new(RefreshResult.Ok, summary);
}
}
@@ -94,10 +101,10 @@ internal class AvatarInfoService : IAvatarInfoService
return (source & define) == define;
}
private async Task<Summary> GetSummaryCoreAsync(PlayerInfo info, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos)
private async Task<Summary> GetSummaryCoreAsync(PlayerInfo info, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos, CancellationToken token)
{
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
Summary summary = await summaryFactory.CreateAsync(info, avatarInfos).ConfigureAwait(false);
Summary summary = await summaryFactory.CreateAsync(info, avatarInfos, token).ConfigureAwait(false);
logger.LogInformation(EventIds.AvatarInfoGeneration, "AvatarInfoSummary Generation toke {time} ms.", stopwatch.GetElapsedTime().TotalMilliseconds);
return summary;
@@ -117,7 +124,7 @@ internal class AvatarInfoService : IAvatarInfoService
foreach (Web.Enka.Model.AvatarInfo webInfo in webInfos)
{
if (webInfo.AvatarId == 10000005 || webInfo.AvatarId == 10000007)
if (AvatarIds.IsPlayer(webInfo.AvatarId))
{
continue;
}
@@ -127,28 +134,31 @@ internal class AvatarInfoService : IAvatarInfoService
if (entity == null)
{
entity = Model.Entity.AvatarInfo.Create(uid, webInfo);
appDbContext.Add(entity);
appDbContext.AvatarInfos.AddAndSave(entity);
}
else
{
entity.Info = webInfo;
appDbContext.Update(entity);
appDbContext.AvatarInfos.UpdateAndSave(entity);
}
}
appDbContext.SaveChanges();
return GetDbAvatarInfos(uid);
}
private List<Web.Enka.Model.AvatarInfo> GetDbAvatarInfos(string uid)
{
return appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.Select(i => i.Info)
// .AsEnumerable()
// .OrderByDescending(i => i.AvatarId)
.ToList();
try
{
return appDbContext.AvatarInfos
.Where(i => i.Uid == uid)
.Select(i => i.Info)
.ToList();
}
catch (ObjectDisposedException)
{
// appDbContext can be disposed unexpectedly
return new();
}
}
}

View File

@@ -16,6 +16,7 @@ internal interface ISummaryFactory
/// </summary>
/// <param name="playerInfo">玩家信息</param>
/// <param name="avatarInfos">角色列表</param>
/// <param name="token">取消令牌</param>
/// <returns>简述对象</returns>
Task<Summary> CreateAsync(PlayerInfo playerInfo, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos);
Task<Summary> CreateAsync(PlayerInfo playerInfo, IEnumerable<Web.Enka.Model.AvatarInfo> avatarInfos, CancellationToken token);
}

View File

@@ -31,15 +31,15 @@ internal class SummaryFactory : ISummaryFactory
}
/// <inheritdoc/>
public async Task<Summary> CreateAsync(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> avatarInfos)
public async Task<Summary> CreateAsync(ModelPlayerInfo playerInfo, IEnumerable<ModelAvatarInfo> avatarInfos, CancellationToken token)
{
Dictionary<int, MetadataAvatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
Dictionary<int, MetadataWeapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
Dictionary<int, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync().ConfigureAwait(false);
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync().ConfigureAwait(false);
Dictionary<int, MetadataAvatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
Dictionary<int, MetadataWeapon> idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
Dictionary<int, FightProperty> idRelicMainPropMap = await metadataService.GetIdToReliquaryMainPropertyMapAsync(token).ConfigureAwait(false);
Dictionary<int, ReliquaryAffix> idReliquaryAffixMap = await metadataService.GetIdReliquaryAffixMapAsync(token).ConfigureAwait(false);
List<ReliquaryLevel> reliqueryLevels = await metadataService.GetReliquaryLevelsAsync().ConfigureAwait(false);
List<MetadataReliquary> reliquaries = await metadataService.GetReliquariesAsync().ConfigureAwait(false);
List<ReliquaryLevel> reliqueryLevels = await metadataService.GetReliquaryLevelsAsync(token).ConfigureAwait(false);
List<MetadataReliquary> reliquaries = await metadataService.GetReliquariesAsync(token).ConfigureAwait(false);
SummaryFactoryImplementation inner = new(idAvatarMap, idWeaponMap, idRelicMainPropMap, idReliquaryAffixMap, reliqueryLevels, reliquaries);
return inner.Create(playerInfo, avatarInfos);

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Model.Binding.AvatarProperty;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Reliquary;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataReliquary = Snap.Hutao.Model.Metadata.Reliquary.Reliquary;
@@ -60,7 +61,7 @@ internal class SummaryFactoryImplementation
return new()
{
Player = SummaryHelper.CreatePlayer(playerInfo),
Avatars = avatarInfos.Select(a =>
Avatars = avatarInfos.Where(a => !AvatarIds.IsPlayer(a.AvatarId)).Select(a =>
{
SummaryAvatarFactory summaryAvatarFactory = new(
idAvatarMap,

View File

@@ -209,7 +209,8 @@ internal static class SummaryHelper
(2, 0) => 100,
(2, 1) => 80,
_ => throw Must.NeverHappen(),
// TODO: Not quite sure why can we hit this branch.
_ => 0,
};
}
@@ -226,16 +227,6 @@ internal static class SummaryHelper
return 100 * ((cr * 2) + cd);
}
private static string FormatValue(FormatMethod method, double value)
{
return method switch
{
FormatMethod.Integer => Math.Round((double)value, MidpointRounding.AwayFromZero).ToString(),
FormatMethod.Percent => string.Format("{0:P1}", value),
_ => value.ToString(),
};
}
private static FightProperty GetBonusFightProperty(IDictionary<FightProperty, double> fightPropMap)
{
if (fightPropMap.ContainsKey(FightProperty.FIGHT_PROP_MAX_FIRE_ENERGY))

View File

@@ -123,15 +123,15 @@ internal class GachaLogService : IGachaLogService, ISupportAsyncInitialization
}
/// <inheritdoc/>
public async ValueTask<bool> InitializeAsync(CancellationToken token = default)
public async ValueTask<bool> InitializeAsync()
{
if (await metadataService.InitializeAsync(token).ConfigureAwait(false))
if (await metadataService.InitializeAsync().ConfigureAwait(false))
{
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync(token).ConfigureAwait(false);
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync(token).ConfigureAwait(false);
nameAvatarMap = await metadataService.GetNameToAvatarMapAsync().ConfigureAwait(false);
nameWeaponMap = await metadataService.GetNameToWeaponMapAsync().ConfigureAwait(false);
idAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
idWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idWeaponMap = await metadataService.GetIdToWeaponMapAsync().ConfigureAwait(false);
IsInitialized = true;
}

View File

@@ -58,7 +58,7 @@ internal interface IGachaLogService
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>是否初始化成功</returns>
ValueTask<bool> InitializeAsync(CancellationToken token = default);
ValueTask<bool> InitializeAsync();
/// <summary>
/// 刷新祈愿记录

View File

@@ -1,6 +1,7 @@
// 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.Context.Database;
@@ -59,7 +60,7 @@ internal class GameService : IGameService
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
SettingEntry entry = appDbContext.Settings.SingleOrAdd(e => e.Key == SettingEntry.GamePath, () => new(SettingEntry.GamePath, null), out bool added);
SettingEntry entry = appDbContext.Settings.SingleOrAdd(e => e.Key == SettingEntry.GamePath, () => new(SettingEntry.GamePath, string.Empty), out bool added);
// Cannot find in setting
if (added)
@@ -89,6 +90,11 @@ internal class GameService : IGameService
}
}
if (entry.Value == null)
{
return new(false, null!);
}
// Set cache and return.
string path = memoryCache.Set(GamePathKey, Must.NotNull(entry.Value!));
return new(true, path);
@@ -139,7 +145,7 @@ internal class GameService : IGameService
public MultiChannel GetMultiChannel()
{
string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(Path.GetDirectoryName(gamePath)!, ConfigFile);
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFile);
using (FileStream stream = File.OpenRead(configPath))
{
@@ -221,6 +227,11 @@ internal class GameService : IGameService
string gamePath = GetGamePathSkipLocator();
if (string.IsNullOrWhiteSpace(gamePath))
{
return;
}
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
string commandLine = new CommandLineBuilder()
.AppendIf("-popupwindow", configuration.IsBorderless)
@@ -317,14 +328,6 @@ internal class GameService : IGameService
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
IQueryable<GameAccount> oldAccounts = appDbContext.GameAccounts.Where(a => a.AttachUid == uid);
foreach (GameAccount account in oldAccounts)
{
account.UpdateAttachUid(null);
appDbContext.GameAccounts.UpdateAndSave(account);
}
gameAccount.UpdateAttachUid(uid);
appDbContext.GameAccounts.UpdateAndSave(gameAccount);
}
@@ -358,7 +361,14 @@ internal class GameService : IGameService
using (IServiceScope scope = scopeFactory.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.GameAccounts.RemoveAndSave(gameAccount);
try
{
appDbContext.GameAccounts.RemoveAndSave(gameAccount);
}
catch (DbUpdateConcurrencyException)
{
// This gameAccount has already been deleted.
}
}
}
}

View File

@@ -46,6 +46,8 @@ internal class ManualGameLocator : IGameLocator
picker.FileTypeFilter.Add(".exe");
picker.SuggestedStartLocation = PickerLocationId.ComputerFolder;
// System.Runtime.InteropServices.COMException (0x80004005): Error HRESULT E_FAIL has been returned from a call to a COM component.
// Not sure what's going on here.
if (await picker.PickSingleFileAsync() is StorageFile file)
{
string path = file.Path;

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.Hutao;
using Snap.Hutao.Model.Metadata;
using Snap.Hutao.Model.Metadata.Avatar;
using Snap.Hutao.Model.Metadata.Weapon;
using Snap.Hutao.Service.Metadata;
@@ -111,8 +112,8 @@ internal class HutaoCache : IHutaoCache
Dictionary<int, Avatar> idAvatarMap = await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false);
idAvatarExtendedMap = new(idAvatarMap)
{
[10000005] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
[10000007] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
[AvatarIds.PlayerBoy] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerBoy", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
[AvatarIds.PlayerGirl] = new() { Name = "旅行者", Icon = "UI_AvatarIcon_PlayerGirl", Quality = Model.Intrinsic.ItemQuality.QUALITY_ORANGE },
};
}

View File

@@ -18,9 +18,8 @@ internal interface IMetadataService
/// <summary>
/// 异步初始化服务,尝试更新元数据
/// </summary>
/// <param name="token">取消令牌</param>
/// <returns>初始化是否成功</returns>
ValueTask<bool> InitializeAsync(CancellationToken token = default);
ValueTask<bool> InitializeAsync();
/// <summary>
/// 异步获取成就列表

View File

@@ -69,7 +69,7 @@ internal partial class MetadataService : IMetadataService, IMetadataInitializer,
public bool IsInitialized { get => isInitialized; private set => isInitialized = value; }
/// <inheritdoc/>
public async ValueTask<bool> InitializeAsync(CancellationToken token = default)
public async ValueTask<bool> InitializeAsync()
{
await initializeCompletionSource.Task.ConfigureAwait(false);
return IsInitialized;

View File

@@ -161,6 +161,7 @@ internal class UserService : IUserService
if (cookie.ContainsSToken())
{
// insert stoken
await ThreadHelper.SwitchToMainThreadAsync();
userWithSameUid.UpdateSToken(uid, cookie);
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);
@@ -169,6 +170,7 @@ internal class UserService : IUserService
if (cookie.ContainsLTokenAndCookieToken())
{
await ThreadHelper.SwitchToMainThreadAsync();
userWithSameUid.Cookie = cookie;
appDbContext.Users.UpdateAndSave(userWithSameUid.Entity);

View File

@@ -24,7 +24,7 @@
<Thickness x:Key="PivotItemMargin">0</Thickness>
</shc:ScopedPage.Resources>
<Grid>
<Grid Visibility="{Binding IsInitialized,Converter={StaticResource BoolToVisibilityConverter}}">
<Rectangle
Height="48"
VerticalAlignment="Top"

View File

@@ -56,10 +56,10 @@
<sc:Setting
Icon="&#xED15;"
Header="反馈"
Description="只处理在 Github 上反馈的问题">
Description="Github 上反馈的问题会优先处理">
<HyperlinkButton
Content="前往反馈"
NavigateUri="http://go.hut.ao/issue"/>
NavigateUri="https://hut.ao/statements/bug-report.html"/>
</sc:Setting>
<sc:SettingExpander>
<sc:SettingExpander.Header>
@@ -75,7 +75,11 @@
IsOpen="True"
CornerRadius="0,0,4,4">
<InfoBar.ActionButton>
<Button HorizontalAlignment="Right" Width="1" Content="没用的按钮"/>
<Button
HorizontalAlignment="Right"
Width="1"
Command="{Binding DebugExceptionCommand}"
Content="没用的按钮"/>
</InfoBar.ActionButton>
</InfoBar>
</sc:SettingExpander>
@@ -105,7 +109,7 @@
</sc:Setting.ActionContent>
</sc:Setting>
</sc:SettingsGroup>
<sc:SettingsGroup Header="测试功能">
<sc:Setting
Icon="&#xEC25;"
@@ -134,7 +138,6 @@
</sc:Setting.ActionContent>
</sc:Setting>
</sc:SettingsGroup>
</StackPanel>
</Grid>

View File

@@ -272,7 +272,7 @@ internal class AchievementViewModel
[ThreadAccess(ThreadAccessState.MainThread)]
private async Task OpenUIAsync()
{
bool metaInitialized = await metadataService.InitializeAsync(CancellationToken).ConfigureAwait(false);
bool metaInitialized = await metadataService.InitializeAsync().ConfigureAwait(false);
if (metaInitialized)
{

View File

@@ -83,7 +83,7 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
{
if (user.SelectedUserGameRole is UserGameRole role)
{
return RefreshCoreAsync((PlayerUid)role, RefreshOption.DatabaseOnly);
return RefreshCoreAsync((PlayerUid)role, RefreshOption.DatabaseOnly, CancellationToken);
}
}
@@ -96,7 +96,7 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
{
if (user.SelectedUserGameRole is UserGameRole role)
{
return RefreshCoreAsync((PlayerUid)role, RefreshOption.Standard);
return RefreshCoreAsync((PlayerUid)role, RefreshOption.Standard, CancellationToken);
}
}
@@ -110,31 +110,37 @@ internal class AvatarPropertyViewModel : ObservableObject, ISupportCancellation
if (isOk)
{
await RefreshCoreAsync(uid, RefreshOption.RequestFromAPI).ConfigureAwait(false);
await RefreshCoreAsync(uid, RefreshOption.RequestFromAPI, CancellationToken).ConfigureAwait(false);
}
}
private async Task RefreshCoreAsync(PlayerUid uid, RefreshOption option)
private async Task RefreshCoreAsync(PlayerUid uid, RefreshOption option, CancellationToken token)
{
(RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(uid, option).ConfigureAwait(false);
try
{
(RefreshResult result, Summary? summary) = await avatarInfoService.GetSummaryAsync(uid, option, token).ConfigureAwait(false);
if (result == RefreshResult.Ok)
{
await ThreadHelper.SwitchToMainThreadAsync();
Summary = summary;
SelectedAvatar = Summary?.Avatars.FirstOrDefault();
}
else
{
switch (result)
if (result == RefreshResult.Ok)
{
case RefreshResult.APIUnavailable:
infoBarService.Warning("角色信息服务当前不可用");
break;
case RefreshResult.ShowcaseNotOpen:
infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试");
break;
await ThreadHelper.SwitchToMainThreadAsync();
Summary = summary;
SelectedAvatar = Summary?.Avatars.FirstOrDefault();
}
else
{
switch (result)
{
case RefreshResult.APIUnavailable:
infoBarService.Warning("角色信息服务当前不可用");
break;
case RefreshResult.ShowcaseNotOpen:
infoBarService.Warning("角色橱窗尚未开启,请前往游戏操作后重试");
break;
}
}
}
catch (OperationCanceledException)
{
}
}
}

View File

@@ -66,15 +66,20 @@ internal class ExperimentalFeaturesViewModel : ObservableObject
{
HomaClient homaClient = Ioc.Default.GetRequiredService<HomaClient>();
IUserService userService = Ioc.Default.GetRequiredService<IUserService>();
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
if (userService.Current is Model.Binding.User user)
{
if (user.SelectedUserGameRole == null)
{
infoBarService.Warning("尚未选择角色");
}
SimpleRecord record = await homaClient.GetPlayerRecordAsync(user).ConfigureAwait(false);
Web.Response.Response<string>? response = await homaClient.UploadRecordAsync(record).ConfigureAwait(false);
if (response != null && response.IsOk())
{
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
infoBarService.Success(response.Message);
}
}

View File

@@ -37,6 +37,7 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
private GachaStatistics? statistics;
private bool isAggressiveRefresh;
private HistoryWish? selectedHistoryWish;
private bool isInitialized;
/// <summary>
/// 构造一个新的祈愿记录视图模型
@@ -112,6 +113,11 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
/// </summary>
public bool IsAggressiveRefresh { get => isAggressiveRefresh; set => SetProperty(ref isAggressiveRefresh, value); }
/// <summary>
/// 初始化是否完成
/// </summary>
public bool IsInitialized { get => isInitialized; set => SetProperty(ref isInitialized, value); }
/// <summary>
/// 页面加载命令
/// </summary>
@@ -183,6 +189,9 @@ internal class GachaLogViewModel : ObservableObject, ISupportCancellation
{
infoBarService.Information("请刷新或导入祈愿记录");
}
await ThreadHelper.SwitchToMainThreadAsync();
IsInitialized = true;
}
}

View File

@@ -13,9 +13,11 @@ using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
using Snap.Hutao.Service.Game;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using System.Collections.ObjectModel;
using System.IO;
namespace Snap.Hutao.ViewModel;
@@ -159,9 +161,9 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
private async Task OpenUIAsync()
{
(bool isOk, string gamePath) = await gameService.GetGamePathAsync().ConfigureAwait(false);
bool gameExists = File.Exists(gameService.GetGamePathSkipLocator());
if (isOk)
if (gameExists)
{
MultiChannel multi = gameService.GetMultiChannel();
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
@@ -170,6 +172,11 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
// Sync from Settings
RetiveSetting();
}
else
{
Ioc.Default.GetRequiredService<IInfoBarService>().Warning("游戏路径不正确,前往设置更改游戏路径。");
await Ioc.Default.GetRequiredService<INavigationService>().NavigateAsync<View.Page.SettingPage>(INavigationAwaiter.Default, true).ConfigureAwait(false);
}
}
private void RetiveSetting()
@@ -209,6 +216,8 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
private async Task LaunchAsync()
{
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
if (gameService.IsGameRunning())
{
return;
@@ -216,7 +225,14 @@ internal class LaunchGameViewModel : ObservableObject, ISupportCancellation
if (SelectedScheme != null)
{
gameService.SetMultiChannel(SelectedScheme);
try
{
gameService.SetMultiChannel(SelectedScheme);
}
catch (UnauthorizedAccessException)
{
infoBarService.Warning("切换服务器失败,保存配置文件时发生异常\n请以管理员模式启动胡桃。");
}
}
if (SelectedGameAccount != null)

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Snap.Hutao.Context.Database;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Threading;
@@ -46,11 +47,13 @@ internal class SettingViewModel : ObservableObject
GamePath = gameService.GetGamePathSkipLocator();
SetGamePathCommand = asyncRelayCommandFactory.Create(SetGamePathAsync);
DebugExceptionCommand = new RelayCommand(DebugThrowException);
}
/// <summary>
/// 版本
/// </summary>
[SuppressMessage("", "CA1822")]
public string AppVersion
{
get => Core.CoreEnvironment.Version.ToString();
@@ -90,6 +93,11 @@ internal class SettingViewModel : ObservableObject
/// </summary>
public ICommand SetGamePathCommand { get; }
/// <summary>
/// 调试异常命令
/// </summary>
public ICommand DebugExceptionCommand { get; }
private async Task SetGamePathAsync()
{
IGameLocator locator = Ioc.Default.GetRequiredService<IEnumerable<IGameLocator>>()
@@ -103,4 +111,11 @@ internal class SettingViewModel : ObservableObject
GamePath = path;
}
}
private void DebugThrowException()
{
#if DEBUG
throw new InvalidOperationException("测试用异常");
#endif
}
}

View File

@@ -161,6 +161,8 @@ internal class WikiAvatarViewModel : ObservableObject
await CombineWithAvatarCollocationsAsync(sorted).ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
// RPC_E_WRONG_THREAD ?
Avatars = new AdvancedCollectionView(sorted, true);
Selected = Avatars.Cast<Avatar>().FirstOrDefault();
}

View File

@@ -8,6 +8,8 @@ namespace Snap.Hutao.Web.Hoyolab;
/// </summary>
public struct PlayerUid
{
[SuppressMessage("", "CA1805")]
[SuppressMessage("", "IDE0079")]
private string? region = default;
/// <summary>

View File

@@ -202,4 +202,4 @@ internal class HomaClient
{
return httpClient.TryCatchPostAsJsonAsync<SimpleRecord, Response<string>>($"{HutaoAPI}/Record/Upload", playerRecord, options, logger, token);
}
}
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hutao.Log;
using Snap.Hutao.Web.Response;
using System.Net.Http;
namespace Snap.Hutao.Web.Hutao;
/// <summary>
/// 胡桃日志客户端
/// </summary>
[HttpClient(HttpClientConfigration.Default)]
internal class HomaClient2
{
private const string HutaoAPI = "https://homa.snapgenshin.com";
private readonly HttpClient httpClient;
private readonly JsonSerializerOptions options;
/// <summary>
/// 构造一个新的胡桃日志客户端
/// </summary>
/// <param name="httpClient">Http客户端</param>
/// <param name="options">Json序列化选项</param>
public HomaClient2(HttpClient httpClient, JsonSerializerOptions options)
{
this.httpClient = httpClient;
this.options = options;
}
/// <summary>
/// 上传日志
/// </summary>
/// <param name="exception">异常</param>
/// <returns>任务</returns>
public async Task<string?> UploadLogAsync(Exception exception)
{
HutaoLog log = new()
{
Id = Core.CoreEnvironment.HutaoDeviceId,
Time = DateTimeOffset.Now.ToUnixTimeMilliseconds(),
Info = exception.ToString(),
};
Response<string>? a = await httpClient
.TryCatchPostAsJsonAsync<HutaoLog, Response<string>>($"{HutaoAPI}/HutaoLog/Upload", log, options)
.ConfigureAwait(false);
return a?.Data;
}
}

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hutao.Log;
/// <summary>
/// 胡桃日志
/// </summary>
public class HutaoLog
{
/// <summary>
/// 设备Id
/// </summary>
public string Id { get; set; } = default!;
/// <summary>
/// 崩溃时间
/// </summary>
public long Time { get; set; }
/// <summary>
/// 错误信息
/// </summary>
public string Info { get; set; } = default!;
}

View File

@@ -14,7 +14,7 @@ public class SimpleRank
/// 构造一个新的数值
/// </summary>
/// <param name="rank">排行</param>
public SimpleRank(Rank rank)
private SimpleRank(Rank rank)
{
AvatarId = rank.AvatarId;
Value = rank.Value;
@@ -29,4 +29,19 @@ public class SimpleRank
/// 值
/// </summary>
public int Value { get; set; }
/// <summary>
/// 构造一个新的简单数值
/// </summary>
/// <param name="rank">排行</param>
/// <returns>新的简单数值</returns>
public static SimpleRank? FromRank(Rank? rank)
{
if (rank == null)
{
return null;
}
return new SimpleRank(rank);
}
}

View File

@@ -17,8 +17,8 @@ public class SimpleSpiralAbyss
public SimpleSpiralAbyss(SpiralAbyss spiralAbyss)
{
ScheduleId = spiralAbyss.ScheduleId;
Damage = new(spiralAbyss.DamageRank.Single());
TakeDamage = new(spiralAbyss.TakeDamageRank.Single());
Damage = SimpleRank.FromRank(spiralAbyss.DamageRank.SingleOrDefault());
TakeDamage = SimpleRank.FromRank(spiralAbyss.TakeDamageRank.SingleOrDefault());
Floors = spiralAbyss.Floors.Select(f => new SimpleFloor(f));
}
@@ -30,12 +30,12 @@ public class SimpleSpiralAbyss
/// <summary>
/// 造成伤害
/// </summary>
public SimpleRank Damage { get; set; } = default!;
public SimpleRank? Damage { get; set; } = default!;
/// <summary>
/// 受到伤害
/// </summary>
public SimpleRank TakeDamage { get; set; } = default!;
public SimpleRank? TakeDamage { get; set; } = default!;
/// <summary>
/// 层