Merge pull request #1746 from DGP-Studio/develop

This commit is contained in:
DismissedLight
2024-06-19 15:46:10 +08:00
committed by GitHub
227 changed files with 3636 additions and 2384 deletions

View File

@@ -19,7 +19,7 @@ body:
- label: 我已阅读 Snap Hutao 文档中的[常见问题](https://hut.ao/advanced/FAQ.html)和[常见程序异常](https://hut.ao/advanced/exceptions.html),我的问题没有在文档中得到解答
required: true
- label: 我知道文档站的导航栏中有**搜索功能**,且已经搜索过相关关键词
- label: 我知道[文档站](https://hut.ao/zh/menu.html)的导航栏中有**搜索功能**,且已经搜索过相关关键词
required: true
- label: 我的问题不是[已完成](https://github.com/DGP-Studio/Snap.Hutao/issues?q=is%3Aopen+is%3Aissue+label%3A%E5%B7%B2%E5%AE%8C%E6%88%90)的问题也不是一个别人已发布的**重复的**问题

View File

@@ -64,12 +64,10 @@ jobs:
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
> [!TIP]
> 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
> 普通用户请 [点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) 下载最新的稳定版本
> [!IMPORTANT]
> 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
>
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 到 `受信任的根证书颁发机构` 以安装测试版安装包
> 请先安装 **[DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt)** 到 **受信任的根证书颁发机构** 以安装测试版安装包
"
echo $summary >> $Env:GITHUB_STEP_SUMMARY
@@ -111,12 +109,10 @@ jobs:
> 该版本是由 CI 程序自动打包生成的 `Alpha` 测试版本,**仅供开发者测试使用**
> [!TIP]
> 普通用户请[点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/)下载最新的稳定版本
> 普通用户请 [点击这里](https://github.com/DGP-Studio/Snap.Hutao/releases/latest/) 下载最新的稳定版本
> [!IMPORTANT]
> 请注意,从 Snap Hutao Alpha 2023.12.21.3 开始,我们将使用全新的 CI 证书,原有的 Snap.Hutao.CI.cer 将在几天后过期停止使用。
>
> 请安装 [DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt) 到 `受信任的根证书颁发机构` 以安装测试版安装包
> 请先安装 **[DGP_Studio_CA.crt](https://github.com/DGP-Automation/Hutao-Auto-Release/releases/download/certificate-ca/DGP_Studio_CA.crt)** 到 **受信任的根证书颁发机构** 以安装测试版安装包
"
echo $summary >> $Env:GITHUB_STEP_SUMMARY

View File

@@ -31,6 +31,7 @@
<ResourceDictionary Source="ms-appx:///Control/Theme/Uri.xaml"/>
<ResourceDictionary Source="ms-appx:///Control/Theme/WindowOverride.xaml"/>
<ResourceDictionary Source="ms-appx:///View/Card/Primitive/CardProgressBar.xaml"/>
<ResourceDictionary Source="ms-appx:///View/Control/RateDeltaTextBlockStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<Style

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Microsoft.Windows.AppNotifications;
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.LifeCycle;
@@ -68,6 +69,10 @@ public sealed partial class App : Application
{
try
{
// Important: You must call AppNotificationManager::Default().Register
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
AppNotificationManager.Default.Register();
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
@@ -81,14 +86,7 @@ public sealed partial class App : Application
LogDiagnosticInformation();
// Manually invoke
HutaoActivationArguments hutaoArgs = HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs);
if (hutaoArgs.Kind is HutaoActivationKind.Toast)
{
Exit();
return;
}
activation.Activate(hutaoArgs);
activation.Activate(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
activation.PostInitialization();
}
catch (Exception ex)

View File

@@ -0,0 +1,51 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Behaviors;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Snap.Hutao.Control.Behavior;
[SuppressMessage("", "CA1001")]
[DependencyProperty("MilliSecondsDelay", typeof(int))]
internal sealed partial class InfoBarDelayCloseBehavior : BehaviorBase<InfoBar>
{
private readonly CancellationTokenSource closeTokenSource = new();
protected override void OnAssociatedObjectLoaded()
{
AssociatedObject.Closed += OnInfoBarClosed;
if (MilliSecondsDelay > 0)
{
DelayCoreAsync().SafeForget();
}
}
private async ValueTask DelayCoreAsync()
{
try
{
await Task.Delay(MilliSecondsDelay, closeTokenSource.Token).ConfigureAwait(true);
}
catch
{
return;
}
if (AssociatedObject is not null)
{
AssociatedObject.IsOpen = false;
}
}
private void OnInfoBarClosed(InfoBar infoBar, InfoBarClosedEventArgs args)
{
if (args.Reason is InfoBarCloseReason.CloseButton)
{
closeTokenSource.Cancel();
}
AssociatedObject.Closed -= OnInfoBarClosed;
}
}

View File

@@ -1,10 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Media.Imaging;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO.DataTransfer;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
namespace Snap.Hutao.Control.Image;
@@ -12,7 +17,9 @@ namespace Snap.Hutao.Control.Image;
/// 缓存图像
/// </summary>
[HighQuality]
internal sealed class CachedImage : Implementation.ImageEx
[DependencyProperty("SourceName", typeof(string), "Unknown")]
[DependencyProperty("CachedName", typeof(string), "Unknown")]
internal sealed partial class CachedImage : Implementation.ImageEx
{
/// <summary>
/// 构造一个新的缓存图像
@@ -26,12 +33,14 @@ internal sealed class CachedImage : Implementation.ImageEx
/// <inheritdoc/>
protected override async Task<Uri?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
{
SourceName = Path.GetFileName(imageUri.ToString());
IImageCache imageCache = this.ServiceProvider().GetRequiredService<IImageCache>();
try
{
HutaoException.ThrowIf(string.IsNullOrEmpty(imageUri.Host), SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true); // BitmapImage need to be created by main thread.
CachedName = Path.GetFileName(file);
token.ThrowIfCancellationRequested(); // check token state to determine whether the operation should be canceled.
return file.ToUri();
}
@@ -42,4 +51,27 @@ internal sealed class CachedImage : Implementation.ImageEx
return default;
}
}
}
[Command("CopyToClipboardCommand")]
private async Task CopyToClipboard()
{
if (Image is Microsoft.UI.Xaml.Controls.Image { Source: BitmapImage bitmap })
{
using (FileStream netStream = File.OpenRead(bitmap.UriSource.LocalPath))
{
using (IRandomAccessStream fxStream = netStream.AsRandomAccessStream())
{
BitmapDecoder decoder = await BitmapDecoder.CreateAsync(fxStream);
SoftwareBitmap softwareBitmap = await decoder.GetSoftwareBitmapAsync(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied);
using (InMemoryRandomAccessStream memory = new())
{
BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, memory);
encoder.SetSoftwareBitmap(softwareBitmap);
await encoder.FlushAsync();
Ioc.Default.GetRequiredService<IClipboardProvider>().SetBitmap(memory);
}
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
<ResourceDictionary
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:shci="using:Snap.Hutao.Control.Image">
@@ -14,6 +14,13 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.ContextFlyout>
<MenuFlyout>
<MenuFlyoutItem IsEnabled="False" Text="{TemplateBinding SourceName}"/>
<MenuFlyoutItem IsEnabled="False" Text="{TemplateBinding CachedName}"/>
<MenuFlyoutItem Command="{Binding CopyToClipboardCommand, RelativeSource={RelativeSource TemplatedParent}}" Text="复制图像"/>
</MenuFlyout>
</Grid.ContextFlyout>
<Image
Name="PlaceholderImage"
Margin="{TemplateBinding PlaceholderMargin}"

View File

@@ -24,7 +24,7 @@
x:Name="ContentGrid"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
x:Load="False">
x:Load="True">
<ContentPresenter.RenderTransform>
<CompositeTransform/>
</ContentPresenter.RenderTransform>

View File

@@ -5,7 +5,6 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Markup;
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.View.Helper;
using Snap.Hutao.ViewModel.Abstraction;

View File

@@ -0,0 +1,25 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Service.Notification;
namespace Snap.Hutao.Control.Selector;
internal sealed class InfoBarTemplateSelector : DataTemplateSelector
{
public DataTemplate ActionButtonEnabled { get; set; } = default!;
public DataTemplate ActionButtonDisabled { get; set; } = default!;
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
{
if (item is InfoBarOptions { ActionButtonContent: { }, ActionButtonCommand: { } })
{
return ActionButtonEnabled;
}
return ActionButtonDisabled;
}
}

View File

@@ -21,8 +21,8 @@ internal sealed partial class SizeRestrictedContentControl : ContentControl
element.Measure(availableSize);
Size contentDesiredSize = element.DesiredSize;
Size contentActualOrDesiredSize = new(
Math.Max(element.ActualWidth, contentDesiredSize.Width),
Math.Max(element.ActualHeight, contentDesiredSize.Height));
Math.Min(Math.Max(element.ActualWidth, contentDesiredSize.Width), availableSize.Width),
Math.Min(Math.Max(element.ActualHeight, contentDesiredSize.Height), availableSize.Height));
if (IsWidthRestricted)
{

View File

@@ -10,6 +10,7 @@
<shmmc:AchievementIconConverter x:Key="AchievementIconConverter"/>
<shmmc:AvatarCardConverter x:Key="AvatarCardConverter"/>
<shmmc:AvatarIconConverter x:Key="AvatarIconConverter"/>
<shmmc:AvatarIconCircleConverter x:Key="AvatarIconCircleConverter"/>
<shmmc:AvatarNameCardPicConverter x:Key="AvatarNameCardPicConverter"/>
<shmmc:AvatarSideIconConverter x:Key="AvatarSideIconConverter"/>
<shmmc:DescriptionsParametersDescriptor x:Key="DescParamDescriptor"/>

View File

@@ -2,4 +2,5 @@
<CornerRadius x:Key="ControlCornerRadiusTop">4,4,0,0</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusBottom">0,0,4,4</CornerRadius>
<CornerRadius x:Key="ControlCornerRadiusTopRightAndBottomLeft">0,4,0,4</CornerRadius>
<CornerRadius x:Key="CornerRadiusAll16">16</CornerRadius>
</ResourceDictionary>

View File

@@ -23,6 +23,9 @@
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing4Template">
<StackPanel Orientation="Horizontal" Spacing="4"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="HorizontalStackPanelSpacing6Template">
<StackPanel Orientation="Horizontal" Spacing="6"/>
</ItemsPanelTemplate>
<ItemsPanelTemplate x:Key="StackPanelSpacing4Template">
<StackPanel Spacing="4"/>
</ItemsPanelTemplate>

View File

@@ -3,7 +3,6 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Snap.Hutao.Win32.Registry;
using System.Linq.Expressions;
using System.Net;
using System.Reflection;

View File

@@ -1,9 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Notifications;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppNotifications;
using Snap.Hutao.Core.LifeCycle.InterProcess;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Shell;
@@ -11,7 +11,6 @@ using Snap.Hutao.Core.Windowing;
using Snap.Hutao.Core.Windowing.HotKey;
using Snap.Hutao.Core.Windowing.NotifyIcon;
using Snap.Hutao.Service;
using Snap.Hutao.Service.DailyNote;
using Snap.Hutao.Service.Discord;
using Snap.Hutao.Service.Hutao;
using Snap.Hutao.Service.Job;
@@ -37,12 +36,10 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
public const string ImportUIAFFromClipboard = nameof(ImportUIAFFromClipboard);
private const string CategoryAchievement = "ACHIEVEMENT";
private const string CategoryDailyNote = "DAILYNOTE";
private const string UrlActionImport = "/IMPORT";
private const string UrlActionRefresh = "/REFRESH";
private readonly IServiceProvider serviceProvider;
private readonly ICurrentXamlWindowReference currentWindowReference;
private readonly IServiceProvider serviceProvider;
private readonly ITaskContext taskContext;
private readonly SemaphoreSlim activateSemaphore = new(1);
@@ -50,36 +47,99 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
/// <inheritdoc/>
public void Activate(HutaoActivationArguments args)
{
HandleActivationAsync(args).SafeForget();
HandleActivationExclusiveAsync(args).SafeForget();
async ValueTask HandleActivationExclusiveAsync(HutaoActivationArguments args)
{
await taskContext.SwitchToBackgroundAsync();
if (activateSemaphore.CurrentCount > 0)
{
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
{
switch (args.Kind)
{
case HutaoActivationKind.Protocol:
{
ArgumentNullException.ThrowIfNull(args.ProtocolActivatedUri);
await HandleProtocolActivationAsync(args.ProtocolActivatedUri, args.IsRedirectTo).ConfigureAwait(false);
break;
}
case HutaoActivationKind.Launch:
{
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
await HandleLaunchActivationAsync(args.IsRedirectTo).ConfigureAwait(false);
break;
}
case HutaoActivationKind.AppNotification:
{
ArgumentNullException.ThrowIfNull(args.AppNotificationActivatedArguments);
await HandleAppNotificationActivationAsync(args.AppNotificationActivatedArguments, args.IsRedirectTo).ConfigureAwait(false);
break;
}
}
}
}
}
}
public void NotificationInvoked(AppNotificationManager manager, AppNotificationActivatedEventArgs args)
{
HandleAppNotificationActivationAsync(args.Arguments, false).SafeForget();
}
/// <inheritdoc/>
public void PostInitialization()
{
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
ToastNotificationManagerCompat.OnActivated += NotificationActivate;
RunPostInitializationAsync().SafeForget();
using (activateSemaphore.Enter())
async ValueTask RunPostInitializationAsync()
{
// TODO: Introduced in 1.10.2, remove in later version
serviceProvider.GetRequiredService<IJumpListInterop>().ClearAsync().SafeForget();
serviceProvider.GetRequiredService<IScheduleTaskInterop>().UnregisterAllTasks();
await taskContext.SwitchToBackgroundAsync();
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
{
return;
// TODO: Introduced in 1.10.2, remove in later version
{
serviceProvider.GetRequiredService<IJumpListInterop>().ClearAsync().SafeForget();
serviceProvider.GetRequiredService<IScheduleTaskInterop>().UnregisterAllTasks();
}
if (UnsafeLocalSetting.Get(SettingKeys.Major1Minor10Revision0GuideState, GuideState.Language) < GuideState.Completed)
{
return;
}
serviceProvider.GetRequiredService<PrivateNamedPipeServer>().RunAsync().SafeForget();
// RegisterHotKey should be called from main thread
await taskContext.SwitchToMainThreadAsync();
serviceProvider.GetRequiredService<HotKeyOptions>().RegisterAll();
if (serviceProvider.GetRequiredService<AppOptions>().IsNotifyIconEnabled)
{
XamlLifetime.ApplicationLaunchedWithNotifyIcon = true;
await taskContext.SwitchToMainThreadAsync();
serviceProvider.GetRequiredService<App>().DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
_ = serviceProvider.GetRequiredService<NotifyIconController>();
}
serviceProvider.GetRequiredService<IDiscordService>().SetNormalActivityAsync().SafeForget();
serviceProvider.GetRequiredService<IQuartzService>().StartAsync().SafeForget();
if (serviceProvider.GetRequiredService<IMetadataService>() is IMetadataServiceInitialization metadataServiceInitialization)
{
metadataServiceInitialization.InitializeInternalAsync().SafeForget();
}
if (serviceProvider.GetRequiredService<IHutaoUserService>() is IHutaoUserServiceInitialization hutaoUserServiceInitialization)
{
hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget();
}
}
serviceProvider.GetRequiredService<HotKeyOptions>().RegisterAll();
if (serviceProvider.GetRequiredService<AppOptions>().IsNotifyIconEnabled)
{
XamlLifetime.ApplicationLaunchedWithNotifyIcon = true;
serviceProvider.GetRequiredService<App>().DispatcherShutdownMode = DispatcherShutdownMode.OnExplicitShutdown;
_ = serviceProvider.GetRequiredService<NotifyIconController>();
}
serviceProvider.GetRequiredService<IQuartzService>().StartAsync(default).SafeForget();
}
}
@@ -124,53 +184,52 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
}
}
private void NotificationActivate(ToastNotificationActivatedEventArgsCompat args)
private async ValueTask HandleProtocolActivationAsync(Uri uri, bool isRedirectTo)
{
ToastArguments toastArgs = ToastArguments.Parse(args.Argument);
UriBuilder builder = new(uri);
if (toastArgs.TryGetValue(Action, out string? action))
{
if (action == LaunchGame)
{
_ = toastArgs.TryGetValue(Uid, out string? uid);
HandleLaunchGameActionAsync(uid).SafeForget();
}
}
}
string category = builder.Host.ToUpperInvariant();
string action = builder.Path.ToUpperInvariant();
private async ValueTask HandleActivationAsync(HutaoActivationArguments args)
{
if (activateSemaphore.CurrentCount > 0)
// string parameter = builder.Query.ToUpperInvariant();
switch (category)
{
using (await activateSemaphore.EnterAsync().ConfigureAwait(false))
{
await HandleActivationCoreAsync(args).ConfigureAwait(false);
}
}
}
private async ValueTask HandleActivationCoreAsync(HutaoActivationArguments args)
{
if (args.Kind is HutaoActivationKind.Protocol)
{
ArgumentNullException.ThrowIfNull(args.ProtocolActivatedUri);
await HandleUrlActivationAsync(args.ProtocolActivatedUri, args.IsRedirectTo).ConfigureAwait(false);
}
else if (args.Kind is HutaoActivationKind.Launch)
{
ArgumentNullException.ThrowIfNull(args.LaunchActivatedArguments);
switch (args.LaunchActivatedArguments)
{
default:
case CategoryAchievement:
{
await WaitMainWindowOrCurrentAsync().ConfigureAwait(false);
if (currentWindowReference.Window is not MainWindow)
{
await HandleNormalLaunchActionAsync(args.IsRedirectTo).ConfigureAwait(false);
break;
// TODO: Send notification to hint?
return;
}
}
switch (action)
{
case UrlActionImport:
{
await taskContext.SwitchToMainThreadAsync();
INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipboard);
await serviceProvider
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.AchievementPage>(navigationAwaiter, true)
.ConfigureAwait(false);
break;
}
}
break;
}
default:
{
await HandleLaunchActivationAsync(isRedirectTo).ConfigureAwait(false);
break;
}
}
}
private async ValueTask HandleNormalLaunchActionAsync(bool isRedirectTo)
private async ValueTask HandleLaunchActivationAsync(bool isRedirectTo)
{
if (!isRedirectTo)
{
@@ -202,6 +261,22 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
await WaitMainWindowOrCurrentAsync().ConfigureAwait(false);
}
private async ValueTask HandleAppNotificationActivationAsync(IDictionary<string, string> arguments, bool isRedirectTo)
{
if (arguments.TryGetValue(Action, out string? action))
{
if (action == LaunchGame)
{
_ = arguments.TryGetValue(Uid, out string? uid);
await HandleLaunchGameActionAsync(uid).ConfigureAwait(false);
}
}
else
{
await HandleLaunchActivationAsync(isRedirectTo).ConfigureAwait(false);
}
}
private async ValueTask WaitMainWindowOrCurrentAsync()
{
if (currentWindowReference.Window is { } window)
@@ -218,100 +293,5 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
mainWindow.SwitchTo();
mainWindow.BringToForeground();
await taskContext.SwitchToBackgroundAsync();
if (serviceProvider.GetRequiredService<IMetadataService>() is IMetadataServiceInitialization metadataServiceInitialization)
{
metadataServiceInitialization.InitializeInternalAsync().SafeForget();
}
if (serviceProvider.GetRequiredService<IHutaoUserService>() is IHutaoUserServiceInitialization hutaoUserServiceInitialization)
{
hutaoUserServiceInitialization.InitializeInternalAsync().SafeForget();
}
serviceProvider.GetRequiredService<IDiscordService>().SetNormalActivityAsync().SafeForget();
}
private async ValueTask HandleUrlActivationAsync(Uri uri, bool isRedirectTo)
{
UriBuilder builder = new(uri);
string category = builder.Host.ToUpperInvariant();
string action = builder.Path.ToUpperInvariant();
string parameter = builder.Query.ToUpperInvariant();
switch (category)
{
case CategoryAchievement:
{
await WaitMainWindowOrCurrentAsync().ConfigureAwait(false);
await HandleAchievementActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
case CategoryDailyNote:
{
await HandleDailyNoteActionAsync(action, parameter, isRedirectTo).ConfigureAwait(false);
break;
}
default:
{
await HandleNormalLaunchActionAsync(isRedirectTo).ConfigureAwait(false);
break;
}
}
}
private async ValueTask HandleAchievementActionAsync(string action, string parameter, bool isRedirectTo)
{
_ = parameter;
_ = isRedirectTo;
switch (action)
{
case UrlActionImport:
{
await taskContext.SwitchToMainThreadAsync();
INavigationAwaiter navigationAwaiter = new NavigationExtra(ImportUIAFFromClipboard);
await serviceProvider
.GetRequiredService<INavigationService>()
.NavigateAsync<View.Page.AchievementPage>(navigationAwaiter, true)
.ConfigureAwait(false);
break;
}
}
}
private async ValueTask HandleDailyNoteActionAsync(string action, string parameter, bool isRedirectTo)
{
_ = parameter;
switch (action)
{
case UrlActionRefresh:
{
try
{
await serviceProvider
.GetRequiredService<IDailyNoteService>()
.RefreshDailyNotesAsync()
.ConfigureAwait(false);
}
catch
{
}
// Check if it's redirected.
if (!isRedirectTo)
{
// It's a direct open process, should exit immediately.
Process.GetCurrentProcess().Kill();
}
break;
}
}
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.Windows.AppLifecycle;
using Microsoft.Windows.AppNotifications;
using Windows.ApplicationModel.Activation;
namespace Snap.Hutao.Core.LifeCycle;
@@ -12,12 +13,6 @@ namespace Snap.Hutao.Core.LifeCycle;
[HighQuality]
internal static class AppActivationArgumentsExtensions
{
/// <summary>
/// 尝试获取协议启动的Uri
/// </summary>
/// <param name="activatedEventArgs">应用程序激活参数</param>
/// <param name="uri">协议Uri</param>
/// <returns>是否存在协议Uri</returns>
public static bool TryGetProtocolActivatedUri(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out Uri? uri)
{
uri = null;
@@ -30,15 +25,10 @@ internal static class AppActivationArgumentsExtensions
return true;
}
/// <summary>
/// 尝试获取启动的参数
/// </summary>
/// <param name="activatedEventArgs">应用程序激活参数</param>
/// <param name="arguments">参数</param>
/// <returns>是否存在参数</returns>
public static bool TryGetLaunchActivatedArguments(this AppActivationArguments activatedEventArgs, [NotNullWhen(true)] out string? arguments)
{
arguments = null;
if (activatedEventArgs.Data is not ILaunchActivatedEventArgs launchArgs)
{
return false;
@@ -47,4 +37,21 @@ internal static class AppActivationArgumentsExtensions
arguments = launchArgs.Arguments.Trim();
return true;
}
public static bool TryGetAppNotificationActivatedArguments(this AppActivationArguments activatedEventArgs, out string? argument, [NotNullWhen(true)] out IDictionary<string, string>? arguments, [NotNullWhen(true)] out IDictionary<string, string>? userInput)
{
argument = null;
arguments = null;
userInput = null;
if (activatedEventArgs.Data is not AppNotificationActivatedEventArgs appNotificationArgs)
{
return false;
}
argument = appNotificationArgs.Argument;
arguments = appNotificationArgs.Arguments;
userInput = appNotificationArgs.UserInput;
return true;
}
}

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.Primitives;
using Microsoft.Windows.AppLifecycle;
namespace Snap.Hutao.Core.LifeCycle;
@@ -10,14 +9,16 @@ internal sealed class HutaoActivationArguments
{
public bool IsRedirectTo { get; set; }
public bool IsToastActivated { get; set; }
public HutaoActivationKind Kind { get; set; }
public Uri? ProtocolActivatedUri { get; set; }
public string? LaunchActivatedArguments { get; set; }
public IDictionary<string, string>? AppNotificationActivatedArguments { get; set; }
public IDictionary<string, string>? AppNotificationActivatedUserInput { get; set; }
public static HutaoActivationArguments FromAppActivationArguments(AppActivationArguments args, bool isRedirected = false)
{
HutaoActivationArguments result = new()
@@ -33,15 +34,6 @@ internal sealed class HutaoActivationArguments
if (args.TryGetLaunchActivatedArguments(out string? arguments))
{
result.LaunchActivatedArguments = arguments;
foreach (StringSegment segment in new StringTokenizer(arguments, [' ']))
{
if (segment.AsSpan().SequenceEqual("-ToastActivated"))
{
result.Kind = HutaoActivationKind.Toast;
break;
}
}
}
break;
@@ -55,6 +47,19 @@ internal sealed class HutaoActivationArguments
result.ProtocolActivatedUri = uri;
}
break;
}
case ExtendedActivationKind.AppNotification:
{
result.Kind = HutaoActivationKind.AppNotification;
if (args.TryGetAppNotificationActivatedArguments(out string? argument, out IDictionary<string, string>? arguments, out IDictionary<string, string>? userInput))
{
result.LaunchActivatedArguments = argument;
result.AppNotificationActivatedArguments = arguments;
result.AppNotificationActivatedUserInput = userInput;
}
break;
}
}

View File

@@ -7,6 +7,6 @@ internal enum HutaoActivationKind
{
None,
Launch,
Toast,
AppNotification,
Protocol,
}

View File

@@ -1,16 +1,15 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Windows.AppNotifications;
namespace Snap.Hutao.Core.LifeCycle;
internal interface IAppActivation
{
void Activate(HutaoActivationArguments args);
void PostInitialization();
}
void NotificationInvoked(AppNotificationManager manager, AppNotificationActivatedEventArgs args);
internal interface IAppActivationActionHandlersAccess
{
ValueTask HandleLaunchGameActionAsync(string? uid = null);
void PostInitialization();
}

View File

@@ -0,0 +1,9 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.LifeCycle;
internal interface IAppActivationActionHandlersAccess
{
ValueTask HandleLaunchGameActionAsync(string? uid = null);
}

View File

@@ -13,6 +13,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
{
private readonly PrivateNamedPipeMessageDispatcher messageDispatcher;
private readonly RuntimeOptions runtimeOptions;
private readonly ILogger<PrivateNamedPipeServer> logger;
private readonly CancellationTokenSource serverTokenSource = new();
private readonly SemaphoreSlim serverSemaphore = new(1);
@@ -23,6 +24,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
{
messageDispatcher = serviceProvider.GetRequiredService<PrivateNamedPipeMessageDispatcher>();
runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
logger = serviceProvider.GetRequiredService<ILogger<PrivateNamedPipeServer>>();
PipeSecurity? pipeSecurity = default;
@@ -64,6 +66,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
try
{
await serverStream.WaitForConnectionAsync(serverTokenSource.Token).ConfigureAwait(false);
logger.LogInformation("Pipe session created");
RunPacketSession(serverStream, serverTokenSource.Token);
}
catch (OperationCanceledException)
@@ -78,6 +81,7 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
while (serverStream.IsConnected && !token.IsCancellationRequested)
{
serverStream.ReadPacket(out PipePacketHeader header);
logger.LogInformation("Pipe packet: [Type:{Type}] [Command:{Command}]", header.Type, header.Command);
switch ((header.Type, header.Command))
{
case (PipePacketType.Request, PipePacketCommand.RequestElevationStatus):
@@ -87,6 +91,11 @@ internal sealed partial class PrivateNamedPipeServer : IDisposable
break;
case (PipePacketType.Request, PipePacketCommand.RedirectActivation):
HutaoActivationArguments? hutaoArgs = serverStream.ReadJsonContent<HutaoActivationArguments>(in header);
if (hutaoArgs is not null)
{
logger.LogInformation("Redirect activation: [Kind:{Kind}] [Arguments:{Arguments}]", hutaoArgs.Kind, hutaoArgs.LaunchActivatedArguments);
}
messageDispatcher.RedirectActivation(hutaoArgs);
break;
case (PipePacketType.SessionTermination, _):

View File

@@ -25,7 +25,7 @@ internal class AsyncBarrier
/// <param name="participants">The number of participants.</param>
public AsyncBarrier(int participants)
{
ArgumentOutOfRangeException.ThrowIfLessThan(participants, 1, "Participants of AsyncBarrier can not be less than 1");
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(participants, "Participants of AsyncBarrier must be greater than 0");
participantCount = participants;
// Allocate the stack so no resizing is necessary.

View File

@@ -4,11 +4,11 @@
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using Windows.Storage;
using static Snap.Hutao.Win32.ConstValues;
namespace Snap.Hutao.Core.Windowing.NotifyIcon;
@@ -26,9 +26,10 @@ internal sealed class NotifyIconController : IDisposable
{
lazyMenu = new(() => new(serviceProvider));
StorageFile iconFile = StorageFile.GetFileFromApplicationUriAsync("ms-appx:///Assets/Logo.ico".ToUri()).AsTask().GetAwaiter().GetResult();
icon = new(iconFile.Path);
id = Unsafe.As<byte, Guid>(ref MemoryMarshal.GetArrayDataReference(MD5.HashData(Encoding.UTF8.GetBytes(iconFile.Path))));
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
string iconPath = Path.Combine(runtimeOptions.InstalledLocation, "Assets/Logo.ico");
icon = new(iconPath);
id = Unsafe.As<byte, Guid>(ref MemoryMarshal.GetArrayDataReference(MD5.HashData(Encoding.UTF8.GetBytes(iconPath))));
xamlHostWindow = new(serviceProvider);
xamlHostWindow.MoveAndResize(default);

View File

@@ -31,6 +31,12 @@ internal static class WindowExtension
return WindowControllers.TryGetValue(window, out _);
}
public static void UninitializeController<TWindow>(this TWindow window)
where TWindow : Window
{
WindowControllers.Remove(window);
}
public static DesktopWindowXamlSource? GetDesktopWindowXamlSource(this Window window)
{
if (window.SystemBackdrop is SystemBackdropDesktopWindowXamlSourceAccess access)

View File

@@ -1,7 +1,6 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Notifications;
using Microsoft.UI;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Content;
@@ -9,6 +8,7 @@ using Microsoft.UI.Input;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.AppNotifications.Builder;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Core.Windowing.Abstraction;
@@ -99,14 +99,13 @@ internal sealed class XamlWindowController
private void OnWindowClosed(object sender, WindowEventArgs args)
{
serviceProvider.GetRequiredService<AppOptions>().PropertyChanged -= OnOptionsPropertyChanged;
if (XamlLifetime.ApplicationLaunchedWithNotifyIcon && !XamlLifetime.ApplicationExiting)
{
args.Handled = true;
window.Hide();
if (!IsNotifyIconVisible())
{
new ToastContentBuilder()
new AppNotificationBuilder()
.AddText(SH.CoreWindowingNotifyIconPromotedHint)
.Show();
}
@@ -119,16 +118,15 @@ internal sealed class XamlWindowController
GC.Collect(GC.MaxGeneration);
}
else
{
if (window is IXamlWindowRectPersisted rectPersisted)
{
SaveOrSkipWindowSize(rectPersisted);
}
subclass?.Dispose();
windowNonRudeHWND?.Dispose();
if (window is IXamlWindowRectPersisted rectPersisted)
{
SaveOrSkipWindowSize(rectPersisted);
}
subclass?.Dispose();
windowNonRudeHWND?.Dispose();
window.UninitializeController();
}
private bool IsNotifyIconVisible()

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Windows.AppNotifications;
using Microsoft.Windows.AppNotifications.Builder;
namespace Snap.Hutao.Extension;
internal static class AppNotificationBuilderExtension
{
/// <summary>
/// Build and show the notification
/// </summary>
/// <param name="builder">this</param>
/// <param name="manager">Defaults to <see cref="AppNotificationManager.Default"/></param>
public static void Show(this AppNotificationBuilder builder, AppNotificationManager? manager = default)
{
(manager ?? AppNotificationManager.Default).Show(builder.BuildNotification());
}
}

View File

@@ -199,6 +199,13 @@ internal static partial class EnumerableExtension
return list;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static List<TSource> SortBy<TSource, TKey>(this List<TSource> list, Func<TSource, TKey> keySelector, Comparison<TKey> comparison)
{
list.Sort((left, right) => comparison(keySelector(left), keySelector(right)));
return list;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static List<TSource> SortByDescending<TSource, TKey>(this List<TSource> list, Func<TSource, TKey> keySelector)
where TKey : IComparable
@@ -213,4 +220,11 @@ internal static partial class EnumerableExtension
list.Sort((left, right) => comparer.Compare(keySelector(right), keySelector(left)));
return list;
}
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public static List<TSource> SortByDescending<TSource, TKey>(this List<TSource> list, Func<TSource, TKey> keySelector, Comparison<TKey> comparison)
{
list.Sort((left, right) => comparison(keySelector(right), keySelector(left)));
return list;
}
}

View File

@@ -4,6 +4,7 @@
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Service;
using System.Collections.Concurrent;
namespace Snap.Hutao.Factory.ContentDialog;
@@ -18,10 +19,27 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
private readonly ITaskContext taskContext;
private readonly AppOptions appOptions;
private readonly ConcurrentQueue<Func<Task>> dialogQueue = [];
private bool isDialogShowing;
public bool IsDialogShowing
{
get
{
if (currentWindowReference.Window is not { } window)
{
return false;
}
return isDialogShowing;
}
}
/// <inheritdoc/>
public async ValueTask<ContentDialogResult> CreateForConfirmAsync(string title, string content)
{
await taskContext.SwitchToMainThreadAsync();
Microsoft.UI.Xaml.Controls.ContentDialog dialog = new()
{
XamlRoot = currentWindowReference.GetXamlRoot(),
@@ -39,6 +57,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
public async ValueTask<ContentDialogResult> CreateForConfirmCancelAsync(string title, string content, ContentDialogButton defaultButton = ContentDialogButton.Close)
{
await taskContext.SwitchToMainThreadAsync();
Microsoft.UI.Xaml.Controls.ContentDialog dialog = new()
{
XamlRoot = currentWindowReference.GetXamlRoot(),
@@ -57,6 +76,7 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
public async ValueTask<Microsoft.UI.Xaml.Controls.ContentDialog> CreateForIndeterminateProgressAsync(string title)
{
await taskContext.SwitchToMainThreadAsync();
Microsoft.UI.Xaml.Controls.ContentDialog dialog = new()
{
XamlRoot = currentWindowReference.GetXamlRoot(),
@@ -72,9 +92,11 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog
{
await taskContext.SwitchToMainThreadAsync();
TContentDialog contentDialog = serviceProvider.CreateInstance<TContentDialog>(parameters);
contentDialog.XamlRoot = currentWindowReference.GetXamlRoot();
contentDialog.RequestedTheme = appOptions.ElementTheme;
return contentDialog;
}
@@ -84,6 +106,51 @@ internal sealed partial class ContentDialogFactory : IContentDialogFactory
TContentDialog contentDialog = serviceProvider.CreateInstance<TContentDialog>(parameters);
contentDialog.XamlRoot = currentWindowReference.GetXamlRoot();
contentDialog.RequestedTheme = appOptions.ElementTheme;
return contentDialog;
}
[SuppressMessage("", "SH003")]
public Task<ContentDialogResult> EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog)
{
TaskCompletionSource<ContentDialogResult> dialogShowCompletionSource = new();
dialogQueue.Enqueue(async () =>
{
try
{
ContentDialogResult result = await contentDialog.ShowAsync();
dialogShowCompletionSource.SetResult(result);
}
catch (Exception ex)
{
dialogShowCompletionSource.SetException(ex);
}
finally
{
ShowNextDialog().SafeForget();
}
});
if (!isDialogShowing)
{
ShowNextDialog();
}
return dialogShowCompletionSource.Task;
Task ShowNextDialog()
{
if (dialogQueue.TryDequeue(out Func<Task>? showNextDialogAsync))
{
isDialogShowing = true;
return showNextDialogAsync();
}
else
{
isDialogShowing = false;
return Task.CompletedTask;
}
}
}
}

View File

@@ -11,6 +11,8 @@ namespace Snap.Hutao.Factory.ContentDialog;
[HighQuality]
internal interface IContentDialogFactory
{
bool IsDialogShowing { get; }
/// <summary>
/// 异步确认
/// </summary>
@@ -40,4 +42,6 @@ internal interface IContentDialogFactory
ValueTask<TContentDialog> CreateInstanceAsync<TContentDialog>(params object[] parameters)
where TContentDialog : Microsoft.UI.Xaml.Controls.ContentDialog;
Task<ContentDialogResult> EnqueueAndShowAsync(Microsoft.UI.Xaml.Controls.ContentDialog contentDialog);
}

View File

@@ -13,7 +13,7 @@ using Windows.Graphics;
namespace Snap.Hutao;
[HighQuality]
[Injection(InjectAs.Singleton)]
[Injection(InjectAs.Transient)]
internal sealed partial class LaunchGameWindow : Window,
IDisposable,
IXamlWindowExtendContentIntoTitleBar,

View File

@@ -14,7 +14,7 @@ namespace Snap.Hutao;
/// 主窗体
/// </summary>
[HighQuality]
[Injection(InjectAs.Singleton)]
[Injection(InjectAs.Transient)]
internal sealed partial class MainWindow : Window,
IXamlWindowExtendContentIntoTitleBar,
IXamlWindowRectPersisted,

View File

@@ -0,0 +1,654 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20240616104646_UidProfilePicture")]
partial class UidProfilePicture
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<uint>("Current")
.HasColumnType("INTEGER");
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<int>("Status")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("achievements");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AchievementArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("achievement_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.AvatarInfo", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CalculatorRefreshTime")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("GameRecordRefreshTime")
.HasColumnType("TEXT");
b.Property<string>("Info")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ShowcaseRefreshTime")
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("avatar_infos");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("Id")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("cultivate_entries");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("AvatarLevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("AvatarLevelTo")
.HasColumnType("INTEGER");
b.Property<Guid>("EntryId")
.HasColumnType("TEXT");
b.Property<uint>("SkillALevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("SkillALevelTo")
.HasColumnType("INTEGER");
b.Property<uint>("SkillELevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("SkillELevelTo")
.HasColumnType("INTEGER");
b.Property<uint>("SkillQLevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("SkillQLevelTo")
.HasColumnType("INTEGER");
b.Property<uint>("WeaponLevelFrom")
.HasColumnType("INTEGER");
b.Property<uint>("WeaponLevelTo")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("EntryId")
.IsUnique();
b.ToTable("cultivate_entry_level_informations");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("Count")
.HasColumnType("INTEGER");
b.Property<Guid>("EntryId")
.HasColumnType("TEXT");
b.Property<bool>("IsFinished")
.HasColumnType("INTEGER");
b.Property<uint>("ItemId")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("EntryId");
b.ToTable("cultivate_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateProject", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachedUid")
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("cultivate_projects");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("DailyNote")
.HasColumnType("TEXT");
b.Property<bool>("DailyTaskNotify")
.HasColumnType("INTEGER");
b.Property<bool>("DailyTaskNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotify")
.HasColumnType("INTEGER");
b.Property<bool>("ExpeditionNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<bool>("HomeCoinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("HomeCoinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RefreshTime")
.HasColumnType("TEXT");
b.Property<bool>("ResinNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<int>("ResinNotifyThreshold")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotify")
.HasColumnType("INTEGER");
b.Property<bool>("TransformerNotifySuppressed")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("UserId");
b.ToTable("daily_notes");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaArchive", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("gacha_archives");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid>("ArchiveId")
.HasColumnType("TEXT");
b.Property<int>("GachaType")
.HasColumnType("INTEGER");
b.Property<long>("Id")
.HasColumnType("INTEGER");
b.Property<uint>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("QueryType")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ArchiveId");
b.ToTable("gacha_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GameAccount", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AttachUid")
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<string>("MihoyoSDK")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.ToTable("game_accounts");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("Count")
.HasColumnType("INTEGER");
b.Property<uint>("ItemId")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("inventory_items");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AppendPropIdList")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("Level")
.HasColumnType("INTEGER");
b.Property<int>("MainPropId")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("inventory_reliquaries");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<int>("ItemId")
.HasColumnType("INTEGER");
b.Property<int>("Level")
.HasColumnType("INTEGER");
b.Property<Guid>("ProjectId")
.HasColumnType("TEXT");
b.Property<int>("PromoteLevel")
.HasColumnType("INTEGER");
b.HasKey("InnerId");
b.HasIndex("ProjectId");
b.ToTable("inventory_weapons");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.ObjectCacheEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("ExpireTime")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("object_cache");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SettingEntry", b =>
{
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Key");
b.ToTable("settings");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.SpiralAbyssEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("ScheduleId")
.HasColumnType("INTEGER");
b.Property<string>("SpiralAbyss")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("spiral_abysses");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.UidProfilePicture", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("AvatarId")
.HasColumnType("INTEGER");
b.Property<uint>("CostumeId")
.HasColumnType("INTEGER");
b.Property<uint>("ProfilePictureId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RefreshTime")
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("uid_profile_pictures");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Aid")
.HasColumnType("TEXT");
b.Property<string>("CookieToken")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("CookieTokenLastUpdateTime")
.HasColumnType("TEXT");
b.Property<string>("Fingerprint")
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("FingerprintLastUpdateTime")
.HasColumnType("TEXT");
b.Property<int>("Index")
.HasColumnType("INTEGER");
b.Property<bool>("IsOversea")
.HasColumnType("INTEGER");
b.Property<bool>("IsSelected")
.HasColumnType("INTEGER");
b.Property<string>("LToken")
.HasColumnType("TEXT")
.HasColumnName("Ltoken");
b.Property<string>("Mid")
.HasColumnType("TEXT");
b.Property<string>("PreferredUid")
.HasColumnType("TEXT");
b.Property<string>("SToken")
.HasColumnType("TEXT")
.HasColumnName("Stoken");
b.HasKey("InnerId");
b.ToTable("users");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.AchievementArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
.WithOne("LevelInformation")
.HasForeignKey("Snap.Hutao.Model.Entity.CultivateEntryLevelInformation", "EntryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entry");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateEntry", "Entry")
.WithMany()
.HasForeignKey("EntryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entry");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.DailyNoteEntry", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.GachaItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.GachaArchive", "Archive")
.WithMany()
.HasForeignKey("ArchiveId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Archive");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryItem", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryReliquary", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.InventoryWeapon", b =>
{
b.HasOne("Snap.Hutao.Model.Entity.CultivateProject", "Project")
.WithMany()
.HasForeignKey("ProjectId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Project");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.CultivateEntry", b =>
{
b.Navigation("LevelInformation");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,39 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations
{
/// <inheritdoc />
public partial class UidProfilePicture : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "uid_profile_pictures",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
Uid = table.Column<string>(type: "TEXT", nullable: false),
ProfilePictureId = table.Column<uint>(type: "INTEGER", nullable: false),
AvatarId = table.Column<uint>(type: "INTEGER", nullable: false),
CostumeId = table.Column<uint>(type: "INTEGER", nullable: false),
RefreshTime = table.Column<DateTimeOffset>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_uid_profile_pictures", x => x.InnerId);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "uid_profile_pictures");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Snap.Hutao.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.2");
modelBuilder.HasAnnotation("ProductVersion", "8.0.6");
modelBuilder.Entity("Snap.Hutao.Model.Entity.Achievement", b =>
{
@@ -466,6 +466,33 @@ namespace Snap.Hutao.Migrations
b.ToTable("spiral_abysses");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.UidProfilePicture", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<uint>("AvatarId")
.HasColumnType("INTEGER");
b.Property<uint>("CostumeId")
.HasColumnType("INTEGER");
b.Property<uint>("ProfilePictureId")
.HasColumnType("INTEGER");
b.Property<DateTimeOffset>("RefreshTime")
.HasColumnType("TEXT");
b.Property<string>("Uid")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("uid_profile_pictures");
});
modelBuilder.Entity("Snap.Hutao.Model.Entity.User", b =>
{
b.Property<Guid>("InnerId")

View File

@@ -65,6 +65,8 @@ internal sealed class AppDbContext : DbContext
public DbSet<SpiralAbyssEntry> SpiralAbysses { get; set; } = default!;
public DbSet<UidProfilePicture> UidProfilePictures { get; set; } = default!;
public static AppDbContext Create(IServiceProvider serviceProvider, string sqlConnectionString)
{
DbContextOptions<AppDbContext> options = new DbContextOptionsBuilder<AppDbContext>()

View File

@@ -12,7 +12,8 @@ namespace Snap.Hutao.Model.Entity;
/// </summary>
[HighQuality]
[Table("inventory_items")]
internal sealed class InventoryItem : IDbMappingForeignKeyFrom<InventoryItem, uint>
internal sealed class InventoryItem : IDbMappingForeignKeyFrom<InventoryItem, uint>,
IDbMappingForeignKeyFrom<InventoryItem, uint, uint>
{
/// <summary>
/// 内部Id
@@ -56,4 +57,21 @@ internal sealed class InventoryItem : IDbMappingForeignKeyFrom<InventoryItem, ui
ItemId = itemId,
};
}
/// <summary>
/// 构造一个新的个数不为0的物品
/// </summary>
/// <param name="projectId">项目Id</param>
/// <param name="itemId">物品Id</param>
/// <param name="count">物品个数</param>
/// <returns>新的个数不为0的物品</returns>
public static InventoryItem From(in Guid projectId, in uint itemId, in uint count)
{
return new()
{
ProjectId = projectId,
ItemId = itemId,
Count = count,
};
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Abstraction;
using Snap.Hutao.Web.Enka.Model;
using Snap.Hutao.Web.Hoyolab;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Model.Entity;
[Table("uid_profile_pictures")]
internal sealed class UidProfilePicture : IMappingFrom<UidProfilePicture, PlayerUid, ProfilePicture>
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
public string Uid { get; set; } = default!;
public uint ProfilePictureId { get; set; }
public uint AvatarId { get; set; }
public uint CostumeId { get; set; }
public DateTimeOffset RefreshTime { get; set; }
[SuppressMessage("", "SH002")]
public static UidProfilePicture From(PlayerUid uid, ProfilePicture profilePicture)
{
return new()
{
Uid = uid.ToString(),
ProfilePictureId = profilePicture.Id,
AvatarId = profilePicture.AvatarId,
CostumeId = profilePicture.CostumeId,
RefreshTime = DateTimeOffset.Now,
};
}
}

View File

@@ -13,7 +13,7 @@ namespace Snap.Hutao.Model.InterChange.GachaLog;
/// UIGF物品
/// </summary>
[HighQuality]
internal sealed class UIGFItem : GachaLogItem, IMappingFrom<UIGFItem, GachaItem, INameQuality>
internal sealed class UIGFItem : GachaLogItem, IMappingFrom<UIGFItem, GachaItem, INameQualityAccess>
{
/// <summary>
/// 额外祈愿映射
@@ -22,7 +22,7 @@ internal sealed class UIGFItem : GachaLogItem, IMappingFrom<UIGFItem, GachaItem,
[JsonEnum(JsonSerializeType.NumberString)]
public GachaType UIGFGachaType { get; set; } = default!;
public static UIGFItem From(GachaItem item, INameQuality nameQuality)
public static UIGFItem From(GachaItem item, INameQualityAccess nameQuality)
{
return new()
{

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Model.Intrinsic;
internal enum ProfilePictureUnlockType
{
None,
Item,
Avatar,
Costume,
ParentQuest,
}

View File

@@ -0,0 +1,13 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Abstraction;
internal interface ICultivationItemsAccess
{
string Name { get; }
List<MaterialId> CultivationItems { get; }
}

View File

@@ -3,7 +3,7 @@
namespace Snap.Hutao.Model.Metadata.Abstraction;
internal interface IItemSource
internal interface IItemConvertible
{
Model.Item ToItem();
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Abstraction;
/// 物品与星级
/// </summary>
[HighQuality]
internal interface INameQuality
internal interface INameQualityAccess
{
/// <summary>
/// 名称

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Metadata.Abstraction;
/// 指示该类为统计物品的源
/// </summary>
[HighQuality]
internal interface IStatisticsItemSource
internal interface IStatisticsItemConvertible
{
/// <summary>
/// 转换到统计物品

View File

@@ -10,19 +10,9 @@ namespace Snap.Hutao.Model.Metadata.Abstraction;
/// 指示该类为简述统计物品的源
/// </summary>
[HighQuality]
internal interface ISummaryItemSource
internal interface ISummaryItemConvertible
{
/// <summary>
/// 星级
/// </summary>
QualityType Quality { get; }
/// <summary>
/// 转换到简述统计物品
/// </summary>
/// <param name="lastPull">距上个五星</param>
/// <param name="time">时间</param>
/// <param name="isUp">是否为Up物品</param>
/// <returns>简述统计物品</returns>
SummaryItem ToSummaryItem(int lastPull, in DateTimeOffset time, bool isUp);
}

View File

@@ -1,99 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.ViewModel.Complex;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.ViewModel.Wiki;
namespace Snap.Hutao.Model.Metadata.Avatar;
/// <summary>
/// 角色的接口实现部分
/// </summary>
internal partial class Avatar : IStatisticsItemSource, ISummaryItemSource, IItemSource, INameQuality, ICalculableSource<ICalculableAvatar>
{
/// <summary>
/// [非元数据] 搭配数据
/// TODO:Add View suffix.
/// </summary>
[JsonIgnore]
public AvatarCollocationView? Collocation { get; set; }
/// <summary>
/// [非元数据] 烹饪奖励
/// </summary>
[JsonIgnore]
public CookBonusView? CookBonusView { get; set; }
/// <summary>
/// [非元数据] 养成物品视图
/// </summary>
[JsonIgnore]
public List<Material>? CultivationItemsView { get; set; }
/// <summary>
/// 最大等级
/// </summary>
[SuppressMessage("", "CA1822")]
public uint MaxLevel { get => GetMaxLevel(); }
public static uint GetMaxLevel()
{
return 90U;
}
/// <inheritdoc/>
public ICalculableAvatar ToCalculable()
{
return CalculableAvatar.From(this);
}
/// <summary>
/// 转换为基础物品
/// </summary>
/// <returns>基础物品</returns>
public Model.Item ToItem()
{
return new()
{
Name = Name,
Icon = AvatarIconConverter.IconNameToUri(Icon),
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
Quality = Quality,
};
}
/// <inheritdoc/>
public StatisticsItem ToStatisticsItem(int count)
{
return new()
{
Name = Name,
Icon = AvatarIconConverter.IconNameToUri(Icon),
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
Quality = Quality,
Count = count,
};
}
/// <inheritdoc/>
public SummaryItem ToSummaryItem(int lastPull, in DateTimeOffset time, bool isUp)
{
return new()
{
Name = Name,
Icon = AvatarIconConverter.IconNameToUri(Icon),
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
Quality = Quality,
Time = time,
LastPull = lastPull,
IsUp = isUp,
};
}
}

View File

@@ -1,99 +1,118 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.Complex;
using Snap.Hutao.ViewModel.GachaLog;
using Snap.Hutao.ViewModel.Wiki;
namespace Snap.Hutao.Model.Metadata.Avatar;
/// <summary>
/// 角色
/// </summary>
[HighQuality]
internal partial class Avatar
internal partial class Avatar : INameQualityAccess,
IStatisticsItemConvertible,
ISummaryItemConvertible,
IItemConvertible,
ICalculableSource<ICalculableAvatar>,
ICultivationItemsAccess
{
/// <summary>
/// Id
/// </summary>
public AvatarId Id { get; set; }
/// <summary>
/// 突破提升 Id 外键
/// </summary>
public PromoteId PromoteId { get; set; }
/// <summary>
/// 排序号
/// </summary>
public uint Sort { get; set; }
/// <summary>
/// 体型
/// </summary>
public BodyType Body { get; set; } = default!;
/// <summary>
/// 正面图标
/// </summary>
public string Icon { get; set; } = default!;
/// <summary>
/// 侧面图标
/// </summary>
public string SideIcon { get; set; } = default!;
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; } = default!;
/// <summary>
/// 角色加入游戏时间
/// </summary>
public DateTimeOffset BeginTime { get; set; }
/// <summary>
/// 星级
/// </summary>
public QualityType Quality { get; set; }
/// <summary>
/// 武器类型
/// </summary>
public WeaponType Weapon { get; set; }
/// <summary>
/// 基础数值
/// </summary>
public AvatarBaseValue BaseValue { get; set; } = default!;
/// <summary>
/// 生长曲线
/// </summary>
public List<TypeValue<FightProperty, GrowCurveType>> GrowCurves { get; set; } = default!;
/// <summary>
/// 技能
/// </summary>
public SkillDepot SkillDepot { get; set; } = default!;
/// <summary>
/// 好感信息/基本信息
/// </summary>
public FetterInfo FetterInfo { get; set; } = default!;
/// <summary>
/// 皮肤
/// </summary>
public List<Costume> Costumes { get; set; } = default!;
/// <summary>
/// 养成物品
/// </summary>
public List<MaterialId> CultivationItems { get; set; } = default!;
[JsonIgnore]
public AvatarCollocationView? CollocationView { get; set; }
[JsonIgnore]
public CookBonusView? CookBonusView { get; set; }
[JsonIgnore]
public List<Material>? CultivationItemsView { get; set; }
[SuppressMessage("", "CA1822")]
public uint MaxLevel { get => GetMaxLevel(); }
public static uint GetMaxLevel()
{
return 90U;
}
public ICalculableAvatar ToCalculable()
{
return CalculableAvatar.From(this);
}
public Model.Item ToItem()
{
return new()
{
Name = Name,
Icon = AvatarIconConverter.IconNameToUri(Icon),
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
Quality = Quality,
};
}
public StatisticsItem ToStatisticsItem(int count)
{
return new()
{
Name = Name,
Icon = AvatarIconConverter.IconNameToUri(Icon),
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
Quality = Quality,
Count = count,
};
}
public SummaryItem ToSummaryItem(int lastPull, in DateTimeOffset time, bool isUp)
{
return new()
{
Name = Name,
Icon = AvatarIconConverter.IconNameToUri(Icon),
Badge = ElementNameIconConverter.ElementNameToIconUri(FetterInfo.VisionBefore),
Quality = Quality,
Time = time,
LastPull = lastPull,
IsUp = isUp,
};
}
}

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Primitive;
namespace Snap.Hutao.Model.Metadata.Avatar;
@@ -12,4 +13,17 @@ internal sealed class ProfilePicture
public string Icon { get; set; } = default!;
public string Name { get; set; } = default!;
}
public ProfilePictureUnlockType UnlockType { get; set; }
/// <summary>
/// <see cref="ProfilePictureUnlockType.Item"/> -> <see cref="MaterialId"/>
/// <br/>
/// <see cref="ProfilePictureUnlockType.Avatar"/> -> <see cref="AvatarId"/>
/// <br/>
/// <see cref="ProfilePictureUnlockType.Costume"/> -> <see cref="CostumeId"/>
/// <br/>
/// <see cref="ProfilePictureUnlockType.ParentQuest"/> -> <see cref="QuestId"/>
/// </summary>
public uint UnlockParameter { get; set; }
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Control;
namespace Snap.Hutao.Model.Metadata.Converter;
/// <summary>
/// 玩家头像转换器
/// </summary>
[HighQuality]
internal sealed class AvatarIconCircleConverter : ValueConverter<string, Uri>
{
/// <summary>
/// 名称转Uri
/// </summary>
/// <param name="name">名称</param>
/// <returns>链接</returns>
public static Uri IconNameToUri(string name)
{
return Web.HutaoEndpoints.StaticRaw("AvatarIconCircle", $"{name}.png").ToUri();
}
/// <inheritdoc/>
public override Uri Convert(string from)
{
return IconNameToUri(from);
}
}

View File

@@ -1,107 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.ViewModel.Complex;
using Snap.Hutao.ViewModel.GachaLog;
namespace Snap.Hutao.Model.Metadata.Weapon;
/// <summary>
/// 武器的接口实现
/// </summary>
internal sealed partial class Weapon : IStatisticsItemSource, ISummaryItemSource, IItemSource, INameQuality, ICalculableSource<ICalculableWeapon>
{
/// <summary>
/// [非元数据] 搭配数据
/// TODO:Add View suffix.
/// </summary>
[JsonIgnore]
public WeaponCollocationView? Collocation { get; set; }
/// <summary>
/// [非元数据] 养成物品视图
/// </summary>
[JsonIgnore]
public List<Material>? CultivationItemsView { get; set; }
/// <inheritdoc cref="INameQuality.Quality" />
[JsonIgnore]
public QualityType Quality
{
get => RankLevel;
}
/// <summary>
/// 最大等级
/// </summary>
internal uint MaxLevel { get => GetMaxLevelByQuality(Quality); }
public static uint GetMaxLevelByQuality(QualityType quality)
{
return quality >= QualityType.QUALITY_BLUE ? 90U : 70U;
}
/// <inheritdoc/>
public ICalculableWeapon ToCalculable()
{
return CalculableWeapon.From(this);
}
/// <summary>
/// 转换为基础物品
/// </summary>
/// <returns>基础物品</returns>
public Model.Item ToItem()
{
return new()
{
Name = Name,
Icon = EquipIconConverter.IconNameToUri(Icon),
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
Quality = RankLevel,
};
}
/// <summary>
/// 转换到统计物品
/// </summary>
/// <param name="count">个数</param>
/// <returns>统计物品</returns>
public StatisticsItem ToStatisticsItem(int count)
{
return new()
{
Name = Name,
Icon = EquipIconConverter.IconNameToUri(Icon),
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
Quality = RankLevel,
Count = count,
};
}
/// <summary>
/// 转换到简述统计物品
/// </summary>
/// <param name="lastPull">距上个五星</param>
/// <param name="time">时间</param>
/// <param name="isUp">是否为Up物品</param>
/// <returns>简述统计物品</returns>
public SummaryItem ToSummaryItem(int lastPull, in DateTimeOffset time, bool isUp)
{
return new()
{
Name = Name,
Icon = EquipIconConverter.IconNameToUri(Icon),
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
Time = time,
Quality = RankLevel,
LastPull = lastPull,
IsUp = isUp,
};
}
}

View File

@@ -1,69 +1,107 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Calculable;
using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Model.Metadata.Converter;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.ViewModel.Complex;
using Snap.Hutao.ViewModel.GachaLog;
namespace Snap.Hutao.Model.Metadata.Weapon;
/// <summary>
/// 武器
/// </summary>
[HighQuality]
internal sealed partial class Weapon
internal sealed partial class Weapon : INameQualityAccess,
IStatisticsItemConvertible,
ISummaryItemConvertible,
IItemConvertible,
ICalculableSource<ICalculableWeapon>,
ICultivationItemsAccess
{
/// <summary>
/// Id
/// </summary>
public WeaponId Id { get; set; }
/// <summary>
/// 突破 Id
/// </summary>
public PromoteId PromoteId { get; set; }
/// <summary>
/// 武器类型
/// </summary>
public uint Sort { get; set; }
public WeaponType WeaponType { get; set; }
/// <summary>
/// 等级
/// </summary>
public QualityType RankLevel { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; } = default!;
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; } = default!;
/// <summary>
/// 图标
/// </summary>
public string Icon { get; set; } = default!;
/// <summary>
/// 觉醒图标
/// </summary>
public string AwakenIcon { get; set; } = default!;
/// <summary>
/// 生长曲线
/// </summary>
public List<WeaponTypeValue> GrowCurves { get; set; } = default!;
/// <summary>
/// 被动信息, 无被动的武器为 <see langword="null"/>
/// </summary>
public NameDescriptions? Affix { get; set; } = default!;
/// <summary>
/// 养成物品
/// </summary>
public List<MaterialId> CultivationItems { get; set; } = default!;
[JsonIgnore]
public WeaponCollocationView? CollocationView { get; set; }
[JsonIgnore]
public List<Material>? CultivationItemsView { get; set; }
[JsonIgnore]
public QualityType Quality
{
get => RankLevel;
}
internal uint MaxLevel { get => GetMaxLevelByQuality(Quality); }
public static uint GetMaxLevelByQuality(QualityType quality)
{
return quality >= QualityType.QUALITY_BLUE ? 90U : 70U;
}
public ICalculableWeapon ToCalculable()
{
return CalculableWeapon.From(this);
}
public Model.Item ToItem()
{
return new()
{
Name = Name,
Icon = EquipIconConverter.IconNameToUri(Icon),
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
Quality = RankLevel,
};
}
public StatisticsItem ToStatisticsItem(int count)
{
return new()
{
Name = Name,
Icon = EquipIconConverter.IconNameToUri(Icon),
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
Quality = RankLevel,
Count = count,
};
}
public SummaryItem ToSummaryItem(int lastPull, in DateTimeOffset time, bool isUp)
{
return new()
{
Name = Name,
Icon = EquipIconConverter.IconNameToUri(Icon),
Badge = WeaponTypeIconConverter.WeaponTypeToIconUri(WeaponType),
Time = time,
Quality = RankLevel,
LastPull = lastPull,
IsUp = isUp,
};
}
}

View File

@@ -13,7 +13,7 @@
<Identity
Name="60568DGPStudio.SnapHutao"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.10.3.0" />
Version="1.10.4.0" />
<Properties>
<DisplayName>Snap Hutao</DisplayName>
@@ -50,7 +50,7 @@
</desktop:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Snap.Hutao.exe" Arguments="-ToastActivated" DisplayName="Snap Hutao Toast Activator">
<com:ExeServer Executable="Snap.Hutao.exe" Arguments="----AppNotificationActivated:" DisplayName="Snap Hutao Toast Activator">
<com:Class Id="5760EC4D-F7E8-4666-A965-9886D7DFFE7D" DisplayName="Snap Hutao Toast Activator"/>
</com:ExeServer>
</com:ComServer>

View File

@@ -13,7 +13,7 @@
<Identity
Name="60568DGPStudio.SnapHutaoDev"
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
Version="1.10.3.0" />
Version="1.10.4.0" />
<Properties>
<DisplayName>Snap Hutao Dev</DisplayName>
@@ -50,7 +50,7 @@
</desktop:Extension>
<com:Extension Category="windows.comServer">
<com:ComServer>
<com:ExeServer Executable="Snap.Hutao.exe" Arguments="-ToastActivated" DisplayName="Snap Hutao Dev Toast Activator">
<com:ExeServer Executable="Snap.Hutao.exe" Arguments="----AppNotificationActivated:" DisplayName="Snap Hutao Toast Activator">
<com:Class Id="F32B561D-752E-472B-A22C-85824D421E1A" DisplayName="Snap Hutao Dev Toast Activator"/>
</com:ExeServer>
</com:ComServer>

View File

@@ -193,16 +193,16 @@
<value>Register [{0}] hotkey [{1}] failed</value>
</data>
<data name="CoreWindowingNotifyIconExitLabel" xml:space="preserve">
<value>退出</value>
<value>Quit</value>
</data>
<data name="CoreWindowingNotifyIconLaunchGameLabel" xml:space="preserve">
<value>Launch</value>
</data>
<data name="CoreWindowingNotifyIconPromotedHint" xml:space="preserve">
<value>胡桃已进入后台运行</value>
<value>Snap Hutao is running in the background</value>
</data>
<data name="CoreWindowingNotifyIconViewLabel" xml:space="preserve">
<value>窗口</value>
<value>Window</value>
</data>
<data name="CoreWindowThemeDark" xml:space="preserve">
<value>Dark</value>
@@ -552,7 +552,7 @@
<value>Refinement {0}</value>
</data>
<data name="MustSelectUserAndUid" xml:space="preserve">
<value>Must sign in to your MiHoYo BBS account and select a user</value>
<value>Must sign in to MiYouShe/HoYoLAB and select a user and role</value>
</data>
<data name="ServerGachaLogServiceDeleteEntrySucceed" xml:space="preserve">
<value>Deleted {1} wish records from UID: {0}</value>
@@ -570,7 +570,7 @@
<value>Found abnormal data, unable to upload to Snap Hutao Cloud. Please do not upload across accounts or you can attempt to delete cloud data and try again.</value>
</data>
<data name="ServerGachaLogServiceUploadEntrySucceed" xml:space="preserve">
<value>Uploaded {1} wish records of UID: {0}, stored {2}</value>
<value>Uploaded {1} wish records to UID: {0}, {2} records saved</value>
</data>
<data name="ServerPassportLoginRequired" xml:space="preserve">
<value>Please login or register Snap Hutao account first</value>
@@ -621,7 +621,7 @@
<value>Verification request is too frequent. Please try again in 1 minute.</value>
</data>
<data name="ServerRecordBannedUid" xml:space="preserve">
<value>Failed to upload Sprial Abyss record, current UID is banned by Hutao Database</value>
<value>Upload Sprial Abyss record failed, current UID is banned by Hutao Database</value>
</data>
<data name="ServerRecordComputingStatistics" xml:space="preserve">
<value>Failed to upload Sprial Abyss record, server is calculating statistical data</value>
@@ -642,7 +642,7 @@
<value>Failed to upload Spiral Abyss record. It is not data for the current schedule.</value>
</data>
<data name="ServerRecordPreviousRequestNotCompleted" xml:space="preserve">
<value>Failed to upload Sprial Abyss record. The record for the current UID is still being processed. Please do not repeat the operation.</value>
<value>Upload Sprial Abyss record failed. The record for the current UID is still being processed. Please do not repeat the operation.</value>
</data>
<data name="ServerRecordUploadSuccessAndGachaLogServiceTimeExtended" xml:space="preserve">
<value>Uploaded Spiral Abyss record successfully. Received a privilege extension for Snap Hutao Cloud service.</value>
@@ -1034,6 +1034,9 @@
<data name="ServiceGameUnlockerReadProcessMemoryPointerAddressFailed" xml:space="preserve">
<value>Error reading process modules' memory: could not read valid value in given address</value>
</data>
<data name="ServiceGameUnlockerWriteProcessMemoryFpsAddressFailed" xml:space="preserve">
<value>Failed to unlock frame ratex{0:X8}</value>
</data>
<data name="ServiceHutaoUserGachaLogExpiredAt" xml:space="preserve">
<value>Wish history data backup service will expire at \n{0:yyyy.MM.dd HH:mm:ss}</value>
</data>
@@ -1373,13 +1376,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>Delete user data permanently?</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<value>Log in Now</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>No Hutao Passport logged in currently, uploading Abyss Records will not grant you Hutao Cloud privilege extension.</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>Log in Now</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>Continue to Upload</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">
@@ -1568,11 +1571,14 @@
<data name="ViewModelCultivationProjectInvalidName" xml:space="preserve">
<value>Can't add plan with invalid name</value>
</data>
<data name="ViewModelCultivationRefreshInventoryProgress" xml:space="preserve">
<value>Syncing inventory items</value>
</data>
<data name="ViewModelCultivationRemoveProjectContent" xml:space="preserve">
<value>此操作不可逆,此计划的养成物品与背包材料将会丢失</value>
<value>This action is irreversible, items and materials in this plan will lose</value>
</data>
<data name="ViewModelCultivationRemoveProjectTitle" xml:space="preserve">
<value>确认要删除当前计划吗?</value>
<value>Are you sure to delete current plan?</value>
</data>
<data name="ViewModelDailyNoteConfigWebhookUrlComplete" xml:space="preserve">
<value>Realtime Note Webhook URL successfully configured</value>
@@ -1925,6 +1931,9 @@
<data name="ViewPageCultivationNavigateAction" xml:space="preserve">
<value>Go</value>
</data>
<data name="ViewPageCultivationRefreshInventory" xml:space="preserve">
<value>Sync Inventory Items</value>
</data>
<data name="ViewPageCultivationRemoveEntry" xml:space="preserve">
<value>Delete list</value>
</data>
@@ -1965,7 +1974,7 @@
<value>Notification Settings</value>
</data>
<data name="ViewPageDailyNoteNotificationUnavailableHint" xml:space="preserve">
<value>胡桃的通知权限已被关闭</value>
<value>Snap Hutao notification permission has been disabled</value>
</data>
<data name="ViewPageDailyNoteRefresh" xml:space="preserve">
<value>Refresh</value>
@@ -1998,7 +2007,7 @@
<value>Refresh</value>
</data>
<data name="ViewPageDailyNoteSettingRefreshNotifyIconDisabledHint" xml:space="preserve">
<value>未启用通知区域图标,关闭窗口后自动刷新将不会执行</value>
<value>Notification area incon is disabled, auto-refresh will be be executed after closing the window</value>
</data>
<data name="ViewPageDailyNoteSlientModeDescription" xml:space="preserve">
<value>Do not disturb during Genshin gaming</value>
@@ -2067,10 +2076,10 @@
<value>Go to Afdian to Buy Related Services</value>
</data>
<data name="ViewPageGachaLogHutaoCloudAfdianPurchaseHeader" xml:space="preserve">
<value>Buy or Renew Cloud Service</value>
<value>Buy / Renew Cloud Services</value>
</data>
<data name="ViewPageGachaLogHutaoCloudDelete" xml:space="preserve">
<value>Delete the Cloud Archive of This UID</value>
<value>Delete cloud archive of this UID</value>
</data>
<data name="ViewPageGachaLogHutaoCloudDeveloperHint" xml:space="preserve">
<value>Lifetime developer license</value>
@@ -2079,7 +2088,7 @@
<value>Snap Hutao Cloud Service Expired</value>
</data>
<data name="ViewPageGachaLogHutaoCloudRetrieve" xml:space="preserve">
<value>Download the Cloud Archive of this UID</value>
<value>Download cloud archive of this UID</value>
</data>
<data name="ViewPageGachaLogHutaoCloudSpiralAbyssActivityDescription" xml:space="preserve">
<value>Free 3-day license after uploading current schedule Abyss record</value>
@@ -2406,7 +2415,7 @@
<value>Rename</value>
</data>
<data name="ViewPageLaunchGameSwitchSchemeDescription" xml:space="preserve">
<value>Switch game server (CN/BiliBili/Global)</value>
<value>Switch game server (CN/Bilibili/Global)</value>
</data>
<data name="ViewPageLaunchGameSwitchSchemeHeader" xml:space="preserve">
<value>Server</value>
@@ -2415,7 +2424,7 @@
<value>You need to convert to a server that matches the launcher before updating the version</value>
</data>
<data name="ViewPageLaunchGameUnlockFpsDescription" xml:space="preserve">
<value>Please turn off V-Sync in the game settings. You may need a high-performance graphic card to support a high frame rate limit.</value>
<value>Please turn off [V-Sync] in your game graphic settings, need high performance GPU to support higher refresh rate gaming</value>
</data>
<data name="ViewPageLaunchGameUnlockFpsHeader" xml:space="preserve">
<value>Unlock Frame Rate Limit</value>
@@ -2433,7 +2442,7 @@
<value>Windows HDR</value>
</data>
<data name="ViewPageLoginHoyoverseUserHint" xml:space="preserve">
<value>Enter your HoYoLab UID</value>
<value>Please input your HoYoLAB UID</value>
</data>
<data name="ViewPageLoginMihoyoUserDescription" xml:space="preserve">
<value>You are using an embedded webview to login to your MiHoYo passport account, the client will fetch the Cookie data when you click on I'm Signed in button. All network communication initialed in this webview occurs only between your computer and MiHoYo official servers.</value>
@@ -2511,7 +2520,7 @@
<value>Unless the developer explicitly asks you to do this, you should not execute the operations below!</value>
</data>
<data name="ViewPageSettingDataFolderDescription" xml:space="preserve">
<value>User data and metadata are saved here</value>
<value>User data/metadata are saved here</value>
</data>
<data name="ViewPageSettingDataFolderHeader" xml:space="preserve">
<value>Data Folder</value>
@@ -2589,7 +2598,7 @@
<value>Select the game server for which you want to get announcements</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionHeader" xml:space="preserve">
<value>Announcement Server</value>
<value>Announcement Game Server Source</value>
</data>
<data name="ViewpageSettingHomeCardDescription" xml:space="preserve">
<value>Manage cards on home dashboard</value>
@@ -2646,7 +2655,7 @@
<value>You are unlimited in any testing feature</value>
</data>
<data name="ViewPageSettingHutaoPassportMaintainerHeader" xml:space="preserve">
<value>Snap Hutao developer and maintainer</value>
<value>Snap Hutao Developer / Maintainer</value>
</data>
<data name="ViewPageSettingHutaoPassportRedeemCodeDescription" xml:space="preserve">
<value>We sometimes give away Snap Hutao Cloud redemption codes to some users</value>
@@ -2679,10 +2688,10 @@
<value>Shortcut Keys</value>
</data>
<data name="ViewPageSettingNotifyIconDescription" xml:space="preserve">
<value>在通知区域显示图标,以允许执行后台任务,重启后生效</value>
<value>Show icon in notification area, to allow background task, restart the program to activate</value>
</data>
<data name="ViewPageSettingNotifyIconHeader" xml:space="preserve">
<value>通知区域图标</value>
<value>Notification Area Icon</value>
</data>
<data name="ViewPageSettingOfficialSiteNavigate" xml:space="preserve">
<value>Official Website</value>
@@ -2703,7 +2712,7 @@
<value>Reset Image Resource</value>
</data>
<data name="ViewPageSettingsAdvancedOptionsLaunchUnlockFpsDescription" xml:space="preserve">
<value>Add Unlock Frame Rate Limit Option in Game Launcher Process Section</value>
<value>Add [Unlock Frame Rate Limit] in [Game Launcher - Process Linkage]</value>
</data>
<data name="ViewPageSettingsAdvancedOptionsLaunchUnlockFpsHeader" xml:space="preserve">
<value>Game Launcher - Unlock Frame Rate Limit</value>
@@ -2751,7 +2760,7 @@
<value>Contribute Translations</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
<value>Display undrawn wish items in Character and Weapon tabs in Wish Export</value>
<value>在「祈愿记录-角色」与「祈愿记录-武器」中显示未抽取到的祈愿物品</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
<value>Undrawn Wish Items</value>
@@ -2997,7 +3006,7 @@
<value>Document</value>
</data>
<data name="ViewUserLoginMihoyoUserDisabledTooltip" xml:space="preserve">
<value>由于米游社安全策略的相关更改,网页登录暂不可用</value>
<value>Due to MiYouShe's security policy changes, login from web is unavailable</value>
</data>
<data name="ViewUserNoUserHint" xml:space="preserve">
<value>Haven't logged in</value>
@@ -3261,7 +3270,7 @@
<value>[{1}] network request exception in [{0}] please try again later</value>
</data>
<data name="WebResponseSignInErrorHint" xml:space="preserve">
<value>登录失败,请前往 HoYoLAB 初始化账号,原始消息:{0}</value>
<value>Login failed, please visit HoYoLAB to initial your account, original message: {0}</value>
</data>
<data name="WindowIdentifyMonitorHeader" xml:space="preserve">
<value>Monitor ID</value>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -1373,13 +1373,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>是否永久删除用户数据</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>继续上传</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -1373,13 +1373,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>Hapus data pengguna secara permanen?</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>继续上传</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -1373,13 +1373,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>ユーザーデータを完全に削除しますか</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>继续上传</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -1373,13 +1373,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>사용자 데이터 영구 삭제</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>继续上传</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -1373,13 +1373,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>Excluir permanentemente os dados do usuário?</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>继续上传</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">

View File

@@ -552,10 +552,10 @@
<value>精炼 {0} 阶</value>
</data>
<data name="MustSelectUserAndUid" xml:space="preserve">
<value>必须登录 米游社/HoYoLAB 并选择一个用户与角色</value>
<value>必须登录 米游社 / HoYoLAB 并选择一个用户与角色</value>
</data>
<data name="ServerGachaLogServiceDeleteEntrySucceed" xml:space="preserve">
<value>删除了 Uid{0} 的 {1} 条祈愿记录</value>
<value>删除了 UID{0} 的 {1} 条祈愿记录</value>
</data>
<data name="ServerGachaLogServiceInsufficientRecordSlot" xml:space="preserve">
<value>胡桃云保存的祈愿记录存档数已达当前账号上限</value>
@@ -570,7 +570,7 @@
<value>数据异常,无法保存至云端,请勿跨账号上传或尝试删除云端数据后重试</value>
</data>
<data name="ServerGachaLogServiceUploadEntrySucceed" xml:space="preserve">
<value>上传了 Uid{0} 的 {1} 条祈愿记录,存储了 {2} 条</value>
<value>上传了 UID{0} 的 {1} 条祈愿记录,存储了 {2} 条</value>
</data>
<data name="ServerPassportLoginRequired" xml:space="preserve">
<value>请先登录或注册胡桃账号</value>
@@ -621,7 +621,7 @@
<value>验证请求过快,请 1 分钟后再试</value>
</data>
<data name="ServerRecordBannedUid" xml:space="preserve">
<value>上传深渊记录失败,当前 Uid 已被胡桃数据库封禁</value>
<value>上传深渊记录失败,当前 UID 已被胡桃数据库封禁</value>
</data>
<data name="ServerRecordComputingStatistics" xml:space="preserve">
<value>上传深渊记录失败,正在计算统计数据</value>
@@ -636,13 +636,13 @@
<value>上传深渊记录失败,存在无效的数据</value>
</data>
<data name="ServerRecordInvalidUid" xml:space="preserve">
<value>无效的 Uid</value>
<value>无效的 UID</value>
</data>
<data name="ServerRecordNotCurrentSchedule" xml:space="preserve">
<value>上传深渊记录失败,不是本期数据</value>
</data>
<data name="ServerRecordPreviousRequestNotCompleted" xml:space="preserve">
<value>上传深渊记录失败,当前 Uid 的记录仍在处理中,请勿重复操作</value>
<value>上传深渊记录失败,当前 UID 的记录仍在处理中,请勿重复操作</value>
</data>
<data name="ServerRecordUploadSuccessAndGachaLogServiceTimeExtended" xml:space="preserve">
<value>上传深渊记录成功,获赠祈愿记录上传服务时长</value>
@@ -1034,6 +1034,9 @@
<data name="ServiceGameUnlockerReadProcessMemoryPointerAddressFailed" xml:space="preserve">
<value>在读取游戏进程内存时遇到问题:无法读取到指定地址的有效值</value>
</data>
<data name="ServiceGameUnlockerWriteProcessMemoryFpsAddressFailed" xml:space="preserve">
<value>解锁帧率失败0x{0:X8}</value>
</data>
<data name="ServiceHutaoUserGachaLogExpiredAt" xml:space="preserve">
<value>祈愿记录上传服务有效期至\n{0:yyyy.MM.dd HH:mm:ss}</value>
</data>
@@ -1373,13 +1376,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>是否永久删除用户数据</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>继续上传</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">
@@ -1545,10 +1548,10 @@
<value>养成计划添加失败</value>
</data>
<data name="ViewModelCultivationBatchAddCompletedFormat" xml:space="preserve">
<value>操作完成:添加/更新:{0} 个,跳过 {1} 个</value>
<value>操作完成:添加 / 更新:{0} 个,跳过 {1} 个</value>
</data>
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
<value>操作未全部完成:添加 / 更新:{0} 个,跳过 {1} 个</value>
</data>
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
<value>已成功添加至当前养成计划</value>
@@ -1568,6 +1571,9 @@
<data name="ViewModelCultivationProjectInvalidName" xml:space="preserve">
<value>不能添加名称无效的计划</value>
</data>
<data name="ViewModelCultivationRefreshInventoryProgress" xml:space="preserve">
<value>正在同步背包物品</value>
</data>
<data name="ViewModelCultivationRemoveProjectContent" xml:space="preserve">
<value>此操作不可逆,此计划的养成物品与背包材料将会丢失</value>
</data>
@@ -1925,6 +1931,9 @@
<data name="ViewPageCultivationNavigateAction" xml:space="preserve">
<value>前往</value>
</data>
<data name="ViewPageCultivationRefreshInventory" xml:space="preserve">
<value>同步背包物品</value>
</data>
<data name="ViewPageCultivationRemoveEntry" xml:space="preserve">
<value>删除清单</value>
</data>
@@ -2067,10 +2076,10 @@
<value>前往爱发电购买相关服务</value>
</data>
<data name="ViewPageGachaLogHutaoCloudAfdianPurchaseHeader" xml:space="preserve">
<value>购买/续费云服务</value>
<value>购买 / 续费云服务</value>
</data>
<data name="ViewPageGachaLogHutaoCloudDelete" xml:space="preserve">
<value>删除此 Uid 的云端存档</value>
<value>删除此 UID 的云端存档</value>
</data>
<data name="ViewPageGachaLogHutaoCloudDeveloperHint" xml:space="preserve">
<value>开发者账号无视服务到期时间</value>
@@ -2079,7 +2088,7 @@
<value>胡桃云服务时长不足</value>
</data>
<data name="ViewPageGachaLogHutaoCloudRetrieve" xml:space="preserve">
<value>下载此 Uid 的云端存档</value>
<value>下载此 UID 的云端存档</value>
</data>
<data name="ViewPageGachaLogHutaoCloudSpiralAbyssActivityDescription" xml:space="preserve">
<value>每期深渊首次上传可免费获得 3 天时长</value>
@@ -2406,7 +2415,7 @@
<value>重命名</value>
</data>
<data name="ViewPageLaunchGameSwitchSchemeDescription" xml:space="preserve">
<value>切换游戏服务器(国服/渠道服/国际服)</value>
<value>切换游戏服务器(国服 / 渠道服 / 国际服)</value>
</data>
<data name="ViewPageLaunchGameSwitchSchemeHeader" xml:space="preserve">
<value>服务器</value>
@@ -2415,7 +2424,7 @@
<value>版本更新前需要提前转换至与启动器匹配的服务器</value>
</data>
<data name="ViewPageLaunchGameUnlockFpsDescription" xml:space="preserve">
<value>请在游戏内关闭垂直同步选项,需要高性能的显卡以支持更高的帧率</value>
<value>请在游戏内关闭垂直同步选项,需要高性能的显卡以支持更高的帧率</value>
</data>
<data name="ViewPageLaunchGameUnlockFpsHeader" xml:space="preserve">
<value>解锁帧率限制</value>
@@ -2433,7 +2442,7 @@
<value>Windows HDR</value>
</data>
<data name="ViewPageLoginHoyoverseUserHint" xml:space="preserve">
<value>请输入你的 HoYoLab Uid</value>
<value>请输入你的 HoYoLab UID</value>
</data>
<data name="ViewPageLoginMihoyoUserDescription" xml:space="preserve">
<value>你正在通过由我们提供的内嵌网页视图登录 米哈游通行证,我们会在你点击 我已登录 按钮后,读取你的 Cookie 信息,由此视图发起的网络通信只发生于你的计算机与米哈游服务器之间</value>
@@ -2511,7 +2520,7 @@
<value>除非开发人员明确要求你这么做,否则不应尝试执行下方的操作!</value>
</data>
<data name="ViewPageSettingDataFolderDescription" xml:space="preserve">
<value>用户数据/元数据 在此处存放</value>
<value>用户数据 / 元数据 在此处存放</value>
</data>
<data name="ViewPageSettingDataFolderHeader" xml:space="preserve">
<value>数据 文件夹</value>
@@ -2589,7 +2598,7 @@
<value>选择想要获取公告的游戏服务器</value>
</data>
<data name="ViewPageSettingHomeAnnouncementRegionHeader" xml:space="preserve">
<value>公告所属服务器</value>
<value>游戏公告所属服务器</value>
</data>
<data name="ViewpageSettingHomeCardDescription" xml:space="preserve">
<value>管理主页仪表板中的卡片</value>
@@ -2646,7 +2655,7 @@
<value>您可以无限制的使用任何测试功能</value>
</data>
<data name="ViewPageSettingHutaoPassportMaintainerHeader" xml:space="preserve">
<value>胡桃开发/运维</value>
<value>胡桃开发 / 运维</value>
</data>
<data name="ViewPageSettingHutaoPassportRedeemCodeDescription" xml:space="preserve">
<value>我们有时会向某些用户赠送胡桃云兑换码</value>
@@ -2703,7 +2712,7 @@
<value>重置图片资源</value>
</data>
<data name="ViewPageSettingsAdvancedOptionsLaunchUnlockFpsDescription" xml:space="preserve">
<value>在启动游戏页面的进程部分加入解锁帧率限制选项</value>
<value>在启动游戏-进程」选项卡加入解锁帧率限制选项</value>
</data>
<data name="ViewPageSettingsAdvancedOptionsLaunchUnlockFpsHeader" xml:space="preserve">
<value>启动游戏-解锁帧率限制</value>
@@ -2751,7 +2760,7 @@
<value>贡献翻译</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
<value>在祈愿记录页面角色与武器页签显示未抽取到的祈愿物品</value>
<value>在祈愿记录-角色」与「祈愿记录-武器」中显示未抽取到的祈愿物品</value>
</data>
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
<value>未抽取到的祈愿物品</value>

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -1373,13 +1373,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>是否永久删除用户数据</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>Продолжить загрузку</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -190,7 +190,7 @@
<value>Không phát hiện WebView2 Runtime</value>
</data>
<data name="CoreWindowHotkeyCombinationRegisterFailed" xml:space="preserve">
<value/>
<value />
</data>
<data name="CoreWindowingNotifyIconExitLabel" xml:space="preserve">
<value>Thoát</value>
@@ -292,7 +292,7 @@
<value>刷新于 {0:MM.dd HH:mm:ss}</value>
</data>
<data name="ModelEntitySpiralAbyssScheduleFormat" xml:space="preserve">
<value/>
<value />
</data>
<data name="ModelInterchangeUIGFItemTypeAvatar" xml:space="preserve">
<value>Tài khoản</value>
@@ -395,7 +395,7 @@
<value>4 Sao</value>
</data>
<data name="ModelIntrinsicItemQualityRed" xml:space="preserve">
<value/>
<value />
</data>
<data name="ModelIntrinsicItemQualityWhite" xml:space="preserve">
<value>1 sao</value>
@@ -669,7 +669,7 @@
<value>Đã tìm thấy nhiều ID thành tựu giống hệt nhau trong kho lưu trữ thành tựu</value>
</data>
<data name="ServiceAvatarInfoPropertyAtk" xml:space="preserve">
<value/>
<value />
<comment>Need EXACT same string in game</comment>
</data>
<data name="ServiceAvatarInfoPropertyBaseAtk" xml:space="preserve">
@@ -1005,7 +1005,7 @@
<value>获取 Package Version 失败</value>
</data>
<data name="ServiceGamePackageRequestScatteredFileFailed" xml:space="preserve">
<value/>
<value />
</data>
<data name="ServiceGamePathLocateFailed" xml:space="preserve">
<value>无法找到游戏路径,请前往设置修改</value>
@@ -1373,13 +1373,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>是否永久删除用户数据</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>前往登录</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>当前未登录胡桃账号,上传深渊数据无法获赠胡桃云时长</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>继续上传</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">

View File

@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
<xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string"/>
<xsd:attribute name="type" type="xsd:string"/>
<xsd:attribute name="mimetype" type="xsd:string"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string"/>
<xsd:attribute name="name" type="xsd:string"/>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
<xsd:attribute ref="xml:space"/>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required"/>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
@@ -1373,13 +1373,13 @@
<data name="ViewDialogSettingDeleteUserDataTitle" xml:space="preserve">
<value>是否永久刪除用戶數據</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginCloseButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<value>前往登入畫面</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginHint" xml:space="preserve">
<value>當前未登入胡桃賬號,上傳深淵數據無法獲贈胡桃雲時長</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginPrimaryButtonText" xml:space="preserve">
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginSecondaryButtonText" xml:space="preserve">
<value>繼續上傳</value>
</data>
<data name="ViewDialogSpiralAbyssUploadRecordHomaNotLoginTitle" xml:space="preserve">

View File

@@ -20,7 +20,7 @@ namespace Snap.Hutao.Service;
[Injection(InjectAs.Scoped, typeof(IAnnouncementService))]
internal sealed partial class AnnouncementService : IAnnouncementService
{
private static readonly string CacheKey = $"{nameof(AnnouncementService)}.Cache.{nameof(AnnouncementWrapper)}";
private const string CacheKey = $"{nameof(AnnouncementService)}.Cache.{nameof(AnnouncementWrapper)}";
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly ITaskContext taskContext;

View File

@@ -16,16 +16,6 @@ internal sealed partial class CultivationDbService : ICultivationDbService
public IServiceProvider ServiceProvider { get => serviceProvider; }
public List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId)
{
return this.List<InventoryItem>(i => i.ProjectId == projectId);
}
public ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default)
{
return this.ListAsync<InventoryItem>(i => i.ProjectId == projectId, token);
}
public ValueTask<List<CultivateEntry>> GetCultivateEntryListByProjectIdAsync(Guid projectId, CancellationToken token = default)
{
return this.ListAsync<CultivateEntry>(e => e.ProjectId == projectId, token);

View File

@@ -2,15 +2,14 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Primitive;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.Service.Inventory;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.ViewModel.Cultivation;
using System.Collections.ObjectModel;
using CalculateItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
using ModelItem = Snap.Hutao.Model.Item;
namespace Snap.Hutao.Service.Cultivation;
@@ -51,22 +50,6 @@ internal sealed partial class CultivationService : ICultivationService
}
}
/// <inheritdoc/>
public List<InventoryItemView> GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand)
{
Guid projectId = cultivateProject.InnerId;
List<InventoryItem> entities = cultivationDbService.GetInventoryItemListByProjectId(projectId);
List<InventoryItemView> results = [];
foreach (Material meta in context.EnumerateInventoryMaterial())
{
InventoryItem entity = entities.SingleOrDefault(e => e.ItemId == meta.Id) ?? InventoryItem.From(projectId, meta.Id);
results.Add(new(entity, meta, saveCommand));
}
return results;
}
/// <inheritdoc/>
public async ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context)
{
@@ -86,7 +69,7 @@ internal sealed partial class CultivationService : ICultivationService
entryItems.Add(new(cultivateItem, context.GetMaterial(cultivateItem.ItemId)));
}
Item item = entry.Type switch
ModelItem item = entry.Type switch
{
CultivateType.AvatarAndSkill => context.GetAvatar(entry.Id).ToItem(),
CultivateType.Weapon => context.GetWeapon(entry.Id).ToItem(),
@@ -130,7 +113,7 @@ internal sealed partial class CultivationService : ICultivationService
}
}
foreach (InventoryItem inventoryItem in await cultivationDbService.GetInventoryItemListByProjectIdAsync(projectId, token).ConfigureAwait(false))
foreach (InventoryItem inventoryItem in await inventoryDbService.GetInventoryItemListByProjectIdAsync(projectId, token).ConfigureAwait(false))
{
if (resultItems.SingleOrDefault(i => i.Inner.Id == inventoryItem.ItemId) is { } existedItem)
{
@@ -147,12 +130,6 @@ internal sealed partial class CultivationService : ICultivationService
await cultivationDbService.RemoveCultivateEntryByIdAsync(entryId).ConfigureAwait(false);
}
/// <inheritdoc/>
public void SaveInventoryItem(InventoryItemView item)
{
inventoryDbService.UpdateInventoryItem(item.Entity);
}
/// <inheritdoc/>
public void SaveCultivateItem(CultivateItemView item)
{

View File

@@ -7,8 +7,7 @@ using System.Collections.ObjectModel;
namespace Snap.Hutao.Service.Cultivation;
internal interface ICultivationDbService : IAppDbService<InventoryItem>,
IAppDbService<CultivateEntryLevelInformation>,
internal interface ICultivationDbService : IAppDbService<CultivateEntryLevelInformation>,
IAppDbService<CultivateProject>,
IAppDbService<CultivateEntry>,
IAppDbService<CultivateItem>
@@ -29,10 +28,6 @@ internal interface ICultivationDbService : IAppDbService<InventoryItem>,
ObservableCollection<CultivateProject> GetCultivateProjectCollection();
List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId);
ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default);
ValueTask AddCultivateEntryAsync(CultivateEntry entry, CancellationToken token = default);
ValueTask AddCultivateItemRangeAsync(IEnumerable<CultivateItem> toAdd, CancellationToken token = default);

View File

@@ -27,8 +27,6 @@ internal interface ICultivationService
ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context);
List<InventoryItemView> GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand);
ValueTask<ObservableCollection<StatisticsCultivateItem>> GetStatisticsCultivateItemCollectionAsync(
CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token);
@@ -54,12 +52,6 @@ internal interface ICultivationService
/// <param name="item">养成物品</param>
void SaveCultivateItem(CultivateItemView item);
/// <summary>
/// 保存单个物品
/// </summary>
/// <param name="item">物品</param>
void SaveInventoryItem(InventoryItemView item);
/// <summary>
/// 异步尝试添加新的项目
/// </summary>

View File

@@ -1,7 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.WinUI.Notifications;
using Microsoft.Windows.AppNotifications;
using Snap.Hutao.Core;
using Snap.Hutao.Core.LifeCycle;
using Snap.Hutao.Model.Entity;
@@ -20,7 +20,6 @@ namespace Snap.Hutao.Service.DailyNote;
[Injection(InjectAs.Singleton)]
internal sealed partial class DailyNoteNotificationOperation
{
private const string ToastHeaderIdArgument = "DAILYNOTE";
private const string ToastAttributionUnknown = "Unknown";
private readonly ITaskContext taskContext;
@@ -52,63 +51,57 @@ internal sealed partial class DailyNoteNotificationOperation
? entry.UserGameRole.ToString()
: await GetUserUidAsync(entry).ConfigureAwait(false);
ToastContentBuilder builder = new ToastContentBuilder()
.AddHeader(ToastHeaderIdArgument, SH.ServiceDailyNoteNotifierTitle, ToastHeaderIdArgument)
.AddAttributionText(attribution)
.AddButton(new ToastButton()
.SetContent(SH.ServiceDailyNoteNotifierActionLaunchGameButton)
.AddArgument(AppActivation.Action, AppActivation.LaunchGame)
.AddArgument(AppActivation.Uid, entry.Uid))
.AddButton(new ToastButtonDismiss(SH.ServiceDailyNoteNotifierActionLaunchGameDismiss));
if (options.IsReminderNotification)
{
builder.SetToastScenario(ToastScenario.Reminder);
}
string reminder = options.IsReminderNotification ? @"scenario=""reminder""" : string.Empty;
string content;
if (notifyInfos.Count > 2)
{
builder.AddText(SH.ServiceDailyNoteNotifierMultiValueReached);
string adaptiveSubgroups = string.Join(string.Empty, notifyInfos.Select(info => $"""
<subgroup>
<text hint-align="center">{info.AdaptiveHint}</text>
<text hint-style="captionSubtle" hint-align="center">{info.Title}</text>
</subgroup>
"""));
// Desktop and Mobile started supporting adaptive toasts in API contract 3 (Anniversary Update)
if (UniversalApiContract.IsPresent(WindowsVersion.Windows10AnniversaryUpdate))
{
AdaptiveGroup group = new();
foreach (DailyNoteNotifyInfo info in notifyInfos)
{
AdaptiveSubgroup subgroup = new()
{
HintWeight = 1,
Children =
{
// new AdaptiveImage() { Source = info.AdaptiveIcon, HintRemoveMargin = true, },
new AdaptiveText() { Text = info.AdaptiveHint, HintAlign = AdaptiveTextAlign.Center, },
new AdaptiveText() { Text = info.Title, HintAlign = AdaptiveTextAlign.Center, HintStyle = AdaptiveTextStyle.CaptionSubtle, },
},
};
group.Children.Add(subgroup);
}
builder.AddVisualChild(group);
}
content = $"""
<text>{SH.ServiceDailyNoteNotifierMultiValueReached}</text>
<group>
{adaptiveSubgroups}
</group>
""";
}
else
{
foreach (DailyNoteNotifyInfo info in notifyInfos)
{
builder.AddText(info.Hint);
}
content = string.Join(string.Empty, notifyInfos.Select(info => $"""
<text>{info.Hint}</text>
"""));
}
string rawXml = $"""
<toast {reminder}>
<header title="{SH.ServiceDailyNoteNotifierTitle}" id="DAILYNOTE" arguments="DAILYNOTE"/>
<visual>
<binding template="ToastGeneric">
{content}
<text placement="attribution">{attribution}</text>
</binding>
</visual>
<actions>
<action activationType="background" content="{SH.ServiceDailyNoteNotifierActionLaunchGameButton}" arguments="{AppActivation.Action}={AppActivation.LaunchGame};{AppActivation.Uid}={entry.Uid}"/>
<action activationType="system" content="{SH.ServiceDailyNoteNotifierActionLaunchGameDismiss}" arguments="dismiss"/>
</actions>
</toast>
""";
AppNotification notification = new(rawXml);
if (options.IsSilentWhenPlayingGame && gameService.IsGameRunning())
{
notification.SuppressDisplay = true;
}
await taskContext.SwitchToMainThreadAsync();
builder.Show(toast => toast.SuppressPopup = ShouldSuppressPopup(options));
}
private bool ShouldSuppressPopup(DailyNoteOptions options)
{
// Prevent notify when we are in game && silent mode.
return options.IsSilentWhenPlayingGame && gameService.IsGameRunning();
AppNotificationManager.Default.Show(notification);
}
private async ValueTask<string> GetUserUidAsync(DailyNoteEntry entry)

View File

@@ -48,7 +48,7 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
{
quartzService.UpdateJobAsync(JobIdentity.DailyNoteGroupName, JobIdentity.DailyNoteRefreshTriggerName, builder =>
{
return builder.WithSimpleSchedule(sb => sb.WithIntervalInMinutes(SelectedRefreshTime.Value).RepeatForever());
return builder.WithSimpleSchedule(sb => sb.WithIntervalInSeconds(SelectedRefreshTime.Value).RepeatForever());
}).SafeForget();
}
}

View File

@@ -49,7 +49,7 @@ internal static class GachaStatisticsExtension
/// <param name="dict">计数器</param>
/// <returns>统计物品列表</returns>
public static List<StatisticsItem> ToStatisticsList<TItem>(this Dictionary<TItem, int> dict)
where TItem : IStatisticsItemSource
where TItem : IStatisticsItemConvertible
{
IOrderedEnumerable<StatisticsItem> result = dict
.Select(kvp => kvp.Key.ToStatisticsItem(kvp.Value))

View File

@@ -27,7 +27,7 @@ internal sealed partial class GachaStatisticsSlimFactory : IGachaStatisticsSlimF
return CreateCore(context, items, uid);
}
private static void Track(INameQuality nameQuality, ref int orangeTracker, ref int purpleTracker)
private static void Track(INameQualityAccess nameQuality, ref int orangeTracker, ref int purpleTracker)
{
switch (nameQuality.Quality)
{
@@ -69,7 +69,7 @@ internal sealed partial class GachaStatisticsSlimFactory : IGachaStatisticsSlimF
// O(n) operation
foreach (ref readonly GachaItem item in CollectionsMarshal.AsSpan(items))
{
INameQuality nameQuality = context.GetNameQualityByItemId(item.ItemId);
INameQualityAccess nameQuality = context.GetNameQualityByItemId(item.ItemId);
switch (item.QueryType)
{
case GachaType.Standard:

View File

@@ -16,11 +16,11 @@ internal sealed class HistoryWishBuilder
{
private readonly GachaEvent gachaEvent;
private readonly Dictionary<IStatisticsItemSource, int> orangeUpCounter = [];
private readonly Dictionary<IStatisticsItemSource, int> purpleUpCounter = [];
private readonly Dictionary<IStatisticsItemSource, int> orangeCounter = [];
private readonly Dictionary<IStatisticsItemSource, int> purpleCounter = [];
private readonly Dictionary<IStatisticsItemSource, int> blueCounter = [];
private readonly Dictionary<IStatisticsItemConvertible, int> orangeUpCounter = [];
private readonly Dictionary<IStatisticsItemConvertible, int> purpleUpCounter = [];
private readonly Dictionary<IStatisticsItemConvertible, int> orangeCounter = [];
private readonly Dictionary<IStatisticsItemConvertible, int> purpleCounter = [];
private readonly Dictionary<IStatisticsItemConvertible, int> blueCounter = [];
private int totalCountTracker;
@@ -37,18 +37,18 @@ internal sealed class HistoryWishBuilder
switch (ConfigType)
{
case GachaType.ActivityAvatar or GachaType.SpecialActivityAvatar:
orangeUpCounter = gachaEvent.UpOrangeList.Select(id => context.IdAvatarMap[id]).ToDictionary(a => (IStatisticsItemSource)a, a => 0);
purpleUpCounter = gachaEvent.UpPurpleList.Select(id => context.IdAvatarMap[id]).ToDictionary(a => (IStatisticsItemSource)a, a => 0);
orangeUpCounter = gachaEvent.UpOrangeList.Select(id => context.IdAvatarMap[id]).ToDictionary(a => (IStatisticsItemConvertible)a, a => 0);
purpleUpCounter = gachaEvent.UpPurpleList.Select(id => context.IdAvatarMap[id]).ToDictionary(a => (IStatisticsItemConvertible)a, a => 0);
break;
case GachaType.ActivityWeapon:
orangeUpCounter = gachaEvent.UpOrangeList.Select(id => context.IdWeaponMap[id]).ToDictionary(w => (IStatisticsItemSource)w, w => 0);
purpleUpCounter = gachaEvent.UpPurpleList.Select(id => context.IdWeaponMap[id]).ToDictionary(w => (IStatisticsItemSource)w, w => 0);
orangeUpCounter = gachaEvent.UpOrangeList.Select(id => context.IdWeaponMap[id]).ToDictionary(w => (IStatisticsItemConvertible)w, w => 0);
purpleUpCounter = gachaEvent.UpPurpleList.Select(id => context.IdWeaponMap[id]).ToDictionary(w => (IStatisticsItemConvertible)w, w => 0);
break;
case GachaType.ActivityCity:
// Avatars are less than weapons, so we try to get the value from avatar map first
orangeUpCounter = gachaEvent.UpOrangeList.Select(id => (IStatisticsItemSource?)context.IdAvatarMap.GetValueOrDefault(id) ?? context.IdWeaponMap[id]).ToDictionary(c => c, c => 0);
purpleUpCounter = gachaEvent.UpPurpleList.Select(id => (IStatisticsItemSource?)context.IdAvatarMap.GetValueOrDefault(id) ?? context.IdWeaponMap[id]).ToDictionary(c => c, c => 0);
orangeUpCounter = gachaEvent.UpOrangeList.Select(id => (IStatisticsItemConvertible?)context.IdAvatarMap.GetValueOrDefault(id) ?? context.IdWeaponMap[id]).ToDictionary(c => c, c => 0);
purpleUpCounter = gachaEvent.UpPurpleList.Select(id => (IStatisticsItemConvertible?)context.IdAvatarMap.GetValueOrDefault(id) ?? context.IdWeaponMap[id]).ToDictionary(c => c, c => 0);
break;
}
}
@@ -74,7 +74,7 @@ internal sealed class HistoryWishBuilder
/// </summary>
/// <param name="item">物品</param>
/// <returns>是否为Up物品</returns>
public bool IncreaseOrange(IStatisticsItemSource item)
public bool IncreaseOrange(IStatisticsItemConvertible item)
{
orangeCounter.IncreaseOne(item);
++totalCountTracker;
@@ -86,7 +86,7 @@ internal sealed class HistoryWishBuilder
/// 计数四星物品
/// </summary>
/// <param name="item">物品</param>
public void IncreasePurple(IStatisticsItemSource item)
public void IncreasePurple(IStatisticsItemConvertible item)
{
purpleUpCounter.TryIncreaseOne(item);
purpleCounter.IncreaseOne(item);
@@ -97,7 +97,7 @@ internal sealed class HistoryWishBuilder
/// 计数三星武器
/// </summary>
/// <param name="item">武器</param>
public void IncreaseBlue(IStatisticsItemSource item)
public void IncreaseBlue(IStatisticsItemConvertible item)
{
blueCounter.IncreaseOne(item);
++totalCountTracker;

View File

@@ -55,7 +55,7 @@ internal sealed class HutaoStatisticsFactory
foreach (ref readonly ItemCount item in CollectionsMarshal.AsSpan(items))
{
IStatisticsItemSource source = item.Item.StringLength() switch
IStatisticsItemConvertible source = item.Item.StringLength() switch
{
8U => context.GetAvatar(item.Item),
5U => context.GetWeapon(item.Item),

View File

@@ -44,7 +44,7 @@ internal sealed class TypedWishSummaryBuilder
/// <param name="item">祈愿物品</param>
/// <param name="source">对应武器</param>
/// <param name="isUp">是否为Up物品</param>
public void Track(GachaItem item, ISummaryItemSource source, bool isUp)
public void Track(GachaItem item, ISummaryItemConvertible source, bool isUp)
{
if (!context.TypeEvaluator(item.GachaType))
{

View File

@@ -59,7 +59,7 @@ internal sealed class GachaLogServiceMetadataContext : IMetadataContext,
return result;
}
public INameQuality GetNameQualityByItemId(uint id)
public INameQualityAccess GetNameQualityByItemId(uint id)
{
uint place = id.StringLength();
return place switch

View File

@@ -3,9 +3,7 @@
using Snap.Hutao.Core;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.Graphics.Direct3D11;
using Windows.Graphics.Capture;
using static Snap.Hutao.Win32.ConstValues;
namespace Snap.Hutao.Service.Game.Automation.ScreenCapture;
@@ -13,20 +11,6 @@ namespace Snap.Hutao.Service.Game.Automation.ScreenCapture;
[Injection(InjectAs.Singleton, typeof(IGameScreenCaptureService))]
internal sealed partial class GameScreenCaptureService : IGameScreenCaptureService
{
private const uint CreateDXGIFactoryFlag =
#if DEBUG
DXGI_CREATE_FACTORY_DEBUG;
#else
0;
#endif
private const D3D11_CREATE_DEVICE_FLAG D3d11CreateDeviceFlag =
D3D11_CREATE_DEVICE_FLAG.D3D11_CREATE_DEVICE_BGRA_SUPPORT
#if DEBUG
| D3D11_CREATE_DEVICE_FLAG.D3D11_CREATE_DEVICE_DEBUG
#endif
;
private readonly ILogger<GameScreenCaptureService> logger;
public bool IsSupported()

View File

@@ -10,8 +10,10 @@ using Snap.Hutao.Model.Intrinsic;
using Snap.Hutao.Service.Game.Configuration;
using Snap.Hutao.Service.Game.Package;
using Snap.Hutao.View.Dialog;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
using Snap.Hutao.Web.Response;
using System.IO;
@@ -43,7 +45,7 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
return;
}
// Backup config file, in order to prevent a incompatible launcher to delete it.
// Backup config file, recover when a incompatible launcher deleted it.
context.ServiceProvider.GetRequiredService<IGameConfigurationFileService>().Backup(gameFileSystem.GameConfigFilePath);
await context.TaskContext.SwitchToMainThreadAsync();
@@ -96,13 +98,30 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
ResourceClient resourceClient = context.ServiceProvider.GetRequiredService<ResourceClient>();
Response<GameResource> response = await resourceClient.GetResourceAsync(context.Scheme).ConfigureAwait(false);
HoyoPlayClient hoyoPlayClient = context.ServiceProvider.GetRequiredService<HoyoPlayClient>();
if (!response.TryGetDataWithoutUINotification(out GameResource? resource))
// We perform these requests before package conversion to ensure resources index is intact.
Response<GamePackagesWrapper> packagesResponse = await hoyoPlayClient.GetPackagesAsync(context.Scheme).ConfigureAwait(false);
if (!packagesResponse.TryGetDataWithoutUINotification(out GamePackagesWrapper? gamePackages))
{
context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse;
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response);
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(packagesResponse);
return false;
}
Response<GameChannelSDKsWrapper> sdkResponse = await hoyoPlayClient.GetChannelSDKAsync(context.Scheme).ConfigureAwait(false);
if (!sdkResponse.TryGetDataWithoutUINotification(out GameChannelSDKsWrapper? channelSDKs))
{
context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse;
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(sdkResponse);
return false;
}
Response<DeprecatedFileConfigurationsWrapper> deprecatedFileResponse = await hoyoPlayClient.GetDeprecatedFileConfigurationsAsync(context.Scheme).ConfigureAwait(false);
if (!deprecatedFileResponse.TryGetDataWithoutUINotification(out DeprecatedFileConfigurationsWrapper? deprecatedFileConfigs))
{
context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse;
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(deprecatedFileResponse);
return false;
}
@@ -110,7 +129,7 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
if (!context.Scheme.ExecutableMatches(gameFileName))
{
if (!await packageConverter.EnsureGameResourceAsync(context.Scheme, resource, gameFolder, progress).ConfigureAwait(false))
if (!await packageConverter.EnsureGameResourceAsync(context.Scheme, gamePackages.GamePackages.Single(), gameFolder, progress).ConfigureAwait(false))
{
context.Result.Kind = LaunchExecutionResultKind.GameResourcePackageConvertInternalError;
context.Result.ErrorMessage = SH.ViewModelLaunchGameEnsureGameResourceFail;
@@ -124,7 +143,7 @@ internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutio
context.Options.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, executableName));
}
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(channelSDKs.GameChannelSDKs.SingleOrDefault(), deprecatedFileConfigs.DeprecatedFileConfigurations.SingleOrDefault(), gameFolder).ConfigureAwait(false);
return true;
}

View File

@@ -20,7 +20,7 @@ internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecution
string configPath = gameFileSystem.GameConfigFilePath;
context.Logger.LogInformation("Game config file path: {ConfigPath}", configPath);
List<IniElement> elements = default!;
List<IniElement> elements;
try
{
elements = [.. IniSerializer.DeserializeFromFile(configPath)];

View File

@@ -13,7 +13,6 @@ internal sealed class PackageConvertStatus
public PackageConvertStatus(string name)
{
Name = name;
Description = name;
}
public PackageConvertStatus(string name, long bytesRead, long totalBytes)

View File

@@ -8,7 +8,9 @@ using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.IO.Http.Sharding;
using Snap.Hutao.Service.Game.Scheme;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher.Resource;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.ChannelSDK;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.DeprecatedFile;
using Snap.Hutao.Web.Hoyolab.HoyoPlay.Connect.Package;
using System.Globalization;
using System.IO;
using System.IO.Compression;
@@ -34,9 +36,9 @@ internal sealed partial class PackageConverter
private readonly HttpClient httpClient;
private readonly ILogger<PackageConverter> logger;
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageConvertStatus> progress)
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GamePackage gamePackage, string gameFolder, IProgress<PackageConvertStatus> progress)
{
// 以 国服 => 国际 为例
// 以 国服 -> 国际 为例
// 1. 下载国际服的 pkg_version 文件,转换为索引字典
// 获取本地对应 pkg_version 文件,转换为索引字典
//
@@ -56,7 +58,7 @@ internal sealed partial class PackageConverter
// 替换操作等于 先备份国服文件,随后新增国际服文件
// 准备下载链接
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
string scatteredFilesUrl = gamePackage.Main.Major.ResourceListUrl;
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.GetDataFolderServerCacheFolder(), gameFolder, scatteredFilesUrl);
@@ -77,7 +79,7 @@ internal sealed partial class PackageConverter
return await ReplaceGameResourceAsync(diffOperations, context, progress).ConfigureAwait(false);
}
public async ValueTask EnsureDeprecatedFilesAndSdkAsync(GameResource resource, string gameFolder)
public async ValueTask EnsureDeprecatedFilesAndSdkAsync(GameChannelSDK? channelSDK, DeprecatedFilesWrapper? deprecatedFiles, string gameFolder)
{
string sdkDllBackup = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll.backup");
string sdkDll = Path.Combine(gameFolder, YuanShenData, "Plugins\\PCGameSDK.dll");
@@ -86,9 +88,9 @@ internal sealed partial class PackageConverter
string sdkVersion = Path.Combine(gameFolder, "sdk_pkg_version");
// Only bilibili's sdk is not null
if (resource.Sdk is not null)
if (channelSDK is not null)
{
using (Stream sdkWebStream = await httpClient.GetStreamAsync(resource.Sdk.Path).ConfigureAwait(false))
using (Stream sdkWebStream = await httpClient.GetStreamAsync(channelSDK.ChannelSdkPackage.Url).ConfigureAwait(false))
{
ZipFile.ExtractToDirectory(sdkWebStream, gameFolder, true);
}
@@ -106,9 +108,9 @@ internal sealed partial class PackageConverter
FileOperation.Move(sdkVersion, sdkVersionBackup, true);
}
if (resource.DeprecatedFiles is not null)
if (deprecatedFiles is not null)
{
foreach (NameMd5 file in resource.DeprecatedFiles)
foreach (DeprecatedFile file in deprecatedFiles.DeprecatedFiles)
{
string filePath = Path.Combine(gameFolder, file.Name);
FileOperation.Move(filePath, $"{filePath}.backup", true);

View File

@@ -30,25 +30,13 @@ internal class LaunchScheme : IEquatable<ChannelOptions>
}
}
/// <summary>
/// 通道
/// </summary>
public ChannelType Channel { get; private protected set; }
/// <summary>
/// 子通道
/// </summary>
public SubChannelType SubChannel { get; private protected set; }
/// <summary>
/// 启动器 Id
/// </summary>
public int LauncherId { get; private protected set; }
public string LauncherId { get; private protected set; } = default!;
/// <summary>
/// API Key
/// </summary>
public string Key { get; private protected set; } = default!;
public string GameId { get; private protected set; } = default!;
/// <summary>
/// 是否为海外

View File

@@ -7,13 +7,13 @@ namespace Snap.Hutao.Service.Game.Scheme;
internal sealed class LaunchSchemeBilibili : LaunchScheme
{
private const int SdkStaticLauncherBilibiliId = 17;
private const string SdkStaticLauncherBilibiliKey = "KAtdSsoQ";
private const string HoyoPlayLauncherBilibiliId = "umfgRO5gh5";
private const string HoyoPlayGameBilibiliId = "T2S0Gz4Dr2";
public LaunchSchemeBilibili(SubChannelType subChannel, bool isNotCompatOnly = true)
{
LauncherId = SdkStaticLauncherBilibiliId;
Key = SdkStaticLauncherBilibiliKey;
LauncherId = HoyoPlayLauncherBilibiliId;
GameId = HoyoPlayGameBilibiliId;
Channel = ChannelType.Bili;
SubChannel = subChannel;
IsOversea = false;

View File

@@ -7,13 +7,13 @@ namespace Snap.Hutao.Service.Game.Scheme;
internal sealed class LaunchSchemeChinese : LaunchScheme
{
private const int SdkStaticLauncherChineseId = 18;
private const string SdkStaticLauncherChineseKey = "eYd89JmJ";
private const string HoyoPlayLauncherChineseId = "jGHBHlcOq1";
private const string HoyoPlayGameChineseId = "1Z8W5NHUQb";
public LaunchSchemeChinese(ChannelType channel, SubChannelType subChannel, bool isNotCompatOnly = true)
{
LauncherId = SdkStaticLauncherChineseId;
Key = SdkStaticLauncherChineseKey;
LauncherId = HoyoPlayLauncherChineseId;
GameId = HoyoPlayGameChineseId;
Channel = channel;
SubChannel = subChannel;
IsOversea = false;

View File

@@ -7,13 +7,13 @@ namespace Snap.Hutao.Service.Game.Scheme;
internal sealed class LaunchSchemeOversea : LaunchScheme
{
private const int SdkStaticLauncherOverseaId = 10;
private const string SdkStaticLauncherOverseaKey = "gcStgarh";
private const string HoyoPlayLauncherOverseaId = "VYTpXlbWo8";
private const string HoyoPlayGameOverseaId = "gopR6Cufr3";
public LaunchSchemeOversea(ChannelType channel, SubChannelType subChannel, bool isNotCompatOnly = true)
{
LauncherId = SdkStaticLauncherOverseaId;
Key = SdkStaticLauncherOverseaKey;
LauncherId = HoyoPlayLauncherOverseaId;
GameId = HoyoPlayGameOverseaId;
Channel = channel;
SubChannel = subChannel;
IsOversea = true;

View File

@@ -3,7 +3,6 @@
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Win32.Foundation;
using Snap.Hutao.Win32.Memory;
using System.Diagnostics;
using static Snap.Hutao.Win32.Kernel32;

View File

@@ -54,6 +54,12 @@ internal sealed class GameFpsUnlocker : IGameFpsUnlocker
if (!context.GameProcess.HasExited && context.FpsAddress != 0U)
{
UnsafeWriteProcessMemory(context.GameProcess, context.FpsAddress, launchOptions.TargetFps);
WIN32_ERROR error = GetLastError();
if (error is not WIN32_ERROR.NO_ERROR)
{
context.Description = SH.FormatServiceGameUnlockerWriteProcessMemoryFpsAddressFailed(error);
}
context.Report();
}
else

View File

@@ -15,30 +15,35 @@ internal readonly struct RequiredLocalModule : IDisposable
public readonly Module UnityPlayer;
public readonly Module UserAssembly;
private readonly HMODULE hModuleUnityPlayer;
private readonly HMODULE hModuleUserAssembly;
[SuppressMessage("", "SH002")]
public RequiredLocalModule(HMODULE unityPlayer, HMODULE userAssembly)
{
hModuleUnityPlayer = unityPlayer;
hModuleUserAssembly = userAssembly;
// Align the pointer
unityPlayer = (nint)(unityPlayer & ~0x3L);
userAssembly = (nint)(userAssembly & ~0x3L);
nint unityPlayerMappedView = (nint)(unityPlayer & ~0x3L);
nint userAssemblyMappedView = (nint)(userAssembly & ~0x3L);
HasValue = true;
UnityPlayer = new((nuint)(nint)unityPlayer, GetImageSize(unityPlayer));
UserAssembly = new((nuint)(nint)userAssembly, GetImageSize(userAssembly));
UnityPlayer = new((nuint)unityPlayerMappedView, GetImageSize(unityPlayerMappedView));
UserAssembly = new((nuint)userAssemblyMappedView, GetImageSize(userAssemblyMappedView));
}
public void Dispose()
{
FreeLibrary((nint)UnityPlayer.Address);
FreeLibrary((nint)UserAssembly.Address);
FreeLibrary(hModuleUnityPlayer);
FreeLibrary(hModuleUserAssembly);
}
[SuppressMessage("", "SH002")]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private unsafe uint GetImageSize(HMODULE hModule)
private unsafe uint GetImageSize(nint baseAddress)
{
IMAGE_DOS_HEADER* pImageDosHeader = (IMAGE_DOS_HEADER*)(nint)hModule;
IMAGE_NT_HEADERS64* pImageNtHeader = (IMAGE_NT_HEADERS64*)(pImageDosHeader->e_lfanew + hModule);
IMAGE_DOS_HEADER* pImageDosHeader = (IMAGE_DOS_HEADER*)baseAddress;
IMAGE_NT_HEADERS64* pImageNtHeader = (IMAGE_NT_HEADERS64*)(pImageDosHeader->e_lfanew + baseAddress);
return pImageNtHeader->OptionalHeader.SizeOfImage;
}
}

View File

@@ -24,51 +24,52 @@ internal sealed partial class HutaoSpiralAbyssService : IHutaoSpiralAbyssService
private readonly IMemoryCache memoryCache;
/// <inheritdoc/>
public ValueTask<Overview> GetOverviewAsync()
public ValueTask<Overview> GetOverviewAsync(bool last = false)
{
return FromCacheOrWebAsync(nameof(Overview), homaClient.GetOverviewAsync);
return FromCacheOrWebAsync(nameof(Overview), last, homaClient.GetOverviewAsync);
}
/// <inheritdoc/>
public ValueTask<List<AvatarAppearanceRank>> GetAvatarAppearanceRanksAsync()
public ValueTask<List<AvatarAppearanceRank>> GetAvatarAppearanceRanksAsync(bool last = false)
{
return FromCacheOrWebAsync(nameof(AvatarAppearanceRank), homaClient.GetAvatarAttendanceRatesAsync);
return FromCacheOrWebAsync(nameof(AvatarAppearanceRank), last, homaClient.GetAvatarAttendanceRatesAsync);
}
/// <inheritdoc/>
public ValueTask<List<AvatarUsageRank>> GetAvatarUsageRanksAsync()
public ValueTask<List<AvatarUsageRank>> GetAvatarUsageRanksAsync(bool last = false)
{
return FromCacheOrWebAsync(nameof(AvatarUsageRank), homaClient.GetAvatarUtilizationRatesAsync);
return FromCacheOrWebAsync(nameof(AvatarUsageRank), last, homaClient.GetAvatarUtilizationRatesAsync);
}
/// <inheritdoc/>
public ValueTask<List<AvatarConstellationInfo>> GetAvatarConstellationInfosAsync()
public ValueTask<List<AvatarConstellationInfo>> GetAvatarConstellationInfosAsync(bool last = false)
{
return FromCacheOrWebAsync(nameof(AvatarConstellationInfo), homaClient.GetAvatarHoldingRatesAsync);
return FromCacheOrWebAsync(nameof(AvatarConstellationInfo), last, homaClient.GetAvatarHoldingRatesAsync);
}
/// <inheritdoc/>
public ValueTask<List<AvatarCollocation>> GetAvatarCollocationsAsync()
public ValueTask<List<AvatarCollocation>> GetAvatarCollocationsAsync(bool last = false)
{
return FromCacheOrWebAsync(nameof(AvatarCollocation), homaClient.GetAvatarCollocationsAsync);
return FromCacheOrWebAsync(nameof(AvatarCollocation), last, homaClient.GetAvatarCollocationsAsync);
}
/// <inheritdoc/>
public ValueTask<List<WeaponCollocation>> GetWeaponCollocationsAsync()
public ValueTask<List<WeaponCollocation>> GetWeaponCollocationsAsync(bool last = false)
{
return FromCacheOrWebAsync(nameof(WeaponCollocation), homaClient.GetWeaponCollocationsAsync);
return FromCacheOrWebAsync(nameof(WeaponCollocation), last, homaClient.GetWeaponCollocationsAsync);
}
/// <inheritdoc/>
public ValueTask<List<TeamAppearance>> GetTeamAppearancesAsync()
public ValueTask<List<TeamAppearance>> GetTeamAppearancesAsync(bool last = false)
{
return FromCacheOrWebAsync(nameof(TeamAppearance), homaClient.GetTeamCombinationsAsync);
return FromCacheOrWebAsync(nameof(TeamAppearance), last, homaClient.GetTeamCombinationsAsync);
}
private async ValueTask<T> FromCacheOrWebAsync<T>(string typeName, Func<CancellationToken, ValueTask<HutaoResponse<T>>> taskFunc)
private async ValueTask<T> FromCacheOrWebAsync<T>(string typeName, bool last, Func<bool, CancellationToken, ValueTask<HutaoResponse<T>>> taskFunc)
where T : class, new()
{
string key = $"{nameof(HutaoSpiralAbyssService)}.Cache.{typeName}";
string kind = last ? "Last" : "Current";
string key = $"{nameof(HutaoSpiralAbyssService)}.Cache.{typeName}.{kind}";
if (memoryCache.TryGetValue(key, out object? cache))
{
T? t = cache as T;
@@ -81,7 +82,7 @@ internal sealed partial class HutaoSpiralAbyssService : IHutaoSpiralAbyssService
return memoryCache.Set(key, value, cacheExpireTime);
}
Response<T> webResponse = await taskFunc(default).ConfigureAwait(false);
Response<T> webResponse = await taskFunc(last, default).ConfigureAwait(false);
T? data = webResponse.Data;
if (data is not null)

View File

@@ -9,6 +9,7 @@ using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.ViewModel.Complex;
using Snap.Hutao.Web.Hutao.SpiralAbyss;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Service.Hutao;
@@ -124,6 +125,32 @@ internal sealed partial class HutaoSpiralAbyssStatisticsCache : IHutaoSpiralAbys
return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static IEnumerable<TResult> CurrentLeftJoinLast<TElement, TKey, TResult>(IEnumerable<TElement> current, IEnumerable<TElement>? last, Func<TElement, TKey> keySelector, Func<TElement, TElement?, TResult> resultSelector)
where TKey : notnull
{
if (last is null)
{
foreach (TElement element in current)
{
yield return resultSelector(element, default);
}
}
else
{
Dictionary<TKey, TElement> lastMap = [];
foreach (TElement element in last)
{
lastMap[keySelector(element)] = element;
}
foreach (TElement element in current)
{
yield return resultSelector(element, lastMap.GetValueOrDefault(keySelector(element)));
}
}
}
private async ValueTask<Dictionary<AvatarId, Avatar>> GetIdAvatarMapExtendedAsync()
{
if (idAvatarExtendedMap is null)
@@ -137,86 +164,91 @@ internal sealed partial class HutaoSpiralAbyssStatisticsCache : IHutaoSpiralAbys
private async ValueTask AvatarCollocationsAsync(Dictionary<AvatarId, Avatar> idAvatarMap, Dictionary<WeaponId, Weapon> idWeaponMap, Dictionary<ExtendedEquipAffixId, Model.Metadata.Reliquary.ReliquarySet> idReliquarySetMap)
{
List<AvatarCollocation> avatarCollocationsRaw;
List<AvatarCollocation> raw, rawLast;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IHutaoSpiralAbyssService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoSpiralAbyssService>();
avatarCollocationsRaw = await hutaoService.GetAvatarCollocationsAsync().ConfigureAwait(false);
raw = await hutaoService.GetAvatarCollocationsAsync(false).ConfigureAwait(false);
rawLast = await hutaoService.GetAvatarCollocationsAsync(true).ConfigureAwait(false);
}
AvatarCollocations = avatarCollocationsRaw.SelectList(co => new AvatarCollocationView()
AvatarCollocations = CurrentLeftJoinLast(raw, rawLast, data => data.AvatarId, (raw, rawLast) => new AvatarCollocationView()
{
AvatarId = co.AvatarId,
Avatars = co.Avatars.SelectList(a => new AvatarView(idAvatarMap[a.Item], a.Rate)),
Weapons = co.Weapons.SelectList(w => new WeaponView(idWeaponMap[w.Item], w.Rate)),
ReliquarySets = co.Reliquaries.SelectList(r => new ReliquarySetView(r, idReliquarySetMap)),
AvatarId = raw.AvatarId,
Avatars = CurrentLeftJoinLast(raw.Avatars, rawLast?.Avatars, data => data.Item, (avatar, avatarLast) => new AvatarView(idAvatarMap[avatar.Item], avatar.Rate, avatarLast?.Rate)).ToList(),
Weapons = CurrentLeftJoinLast(raw.Weapons, rawLast?.Weapons, data => data.Item, (weapon, weaponLast) => new WeaponView(idWeaponMap[weapon.Item], weapon.Rate, weaponLast?.Rate)).ToList(),
ReliquarySets = CurrentLeftJoinLast(raw.Reliquaries, rawLast?.Reliquaries, data => data.Item, (relic, relicLast) => new ReliquarySetView(idReliquarySetMap, relic, relicLast)).ToList(),
}).ToDictionary(a => a.AvatarId);
}
private async ValueTask WeaponCollocationsAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<WeaponCollocation> weaponCollocationsRaw;
List<WeaponCollocation> raw, rawLast;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IHutaoSpiralAbyssService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoSpiralAbyssService>();
weaponCollocationsRaw = await hutaoService.GetWeaponCollocationsAsync().ConfigureAwait(false);
raw = await hutaoService.GetWeaponCollocationsAsync(false).ConfigureAwait(false);
rawLast = await hutaoService.GetWeaponCollocationsAsync(true).ConfigureAwait(false);
}
WeaponCollocations = weaponCollocationsRaw.SelectList(co => new WeaponCollocationView()
WeaponCollocations = CurrentLeftJoinLast(raw, rawLast, data => data.WeaponId, (raw, rawLast) => new WeaponCollocationView()
{
WeaponId = co.WeaponId,
Avatars = co.Avatars.SelectList(a => new AvatarView(idAvatarMap[a.Item], a.Rate)),
WeaponId = raw.WeaponId,
Avatars = CurrentLeftJoinLast(raw.Avatars, rawLast?.Avatars, data => data.Item, (avatar, avatarLast) => new AvatarView(idAvatarMap[avatar.Item], avatar.Rate, avatarLast?.Rate)).ToList(),
}).ToDictionary(w => w.WeaponId);
}
[SuppressMessage("", "SH003")]
private async Task AvatarAppearanceRankAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<AvatarAppearanceRank> avatarAppearanceRanksRaw;
List<AvatarAppearanceRank> raw, rawLast;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IHutaoSpiralAbyssService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoSpiralAbyssService>();
avatarAppearanceRanksRaw = await hutaoService.GetAvatarAppearanceRanksAsync().ConfigureAwait(false);
raw = await hutaoService.GetAvatarAppearanceRanksAsync(false).ConfigureAwait(false);
rawLast = await hutaoService.GetAvatarAppearanceRanksAsync(true).ConfigureAwait(false);
}
AvatarAppearanceRanks = avatarAppearanceRanksRaw.SortByDescending(r => r.Floor).SelectList(rank => new AvatarRankView
AvatarAppearanceRanks = CurrentLeftJoinLast(raw.SortByDescending(r => r.Floor), rawLast, data => data.Floor, (raw, rawLast) => new AvatarRankView
{
Floor = SH.FormatModelBindingHutaoComplexRankFloor(rank.Floor),
Avatars = rank.Ranks.SortByDescending(r => r.Rate).SelectList(rank => new AvatarView(idAvatarMap[rank.Item], rank.Rate)),
});
Floor = SH.FormatModelBindingHutaoComplexRankFloor(raw.Floor),
Avatars = CurrentLeftJoinLast(raw.Ranks.SortByDescending(r => r.Rate), rawLast?.Ranks, data => data.Item, (rank, rankLast) => new AvatarView(idAvatarMap[rank.Item], rank.Rate, rankLast?.Rate)).ToList(),
}).ToList();
}
[SuppressMessage("", "SH003")]
private async Task AvatarUsageRanksAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<AvatarUsageRank> avatarUsageRanksRaw;
List<AvatarUsageRank> raw, rawLast;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IHutaoSpiralAbyssService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoSpiralAbyssService>();
avatarUsageRanksRaw = await hutaoService.GetAvatarUsageRanksAsync().ConfigureAwait(false);
raw = await hutaoService.GetAvatarUsageRanksAsync(false).ConfigureAwait(false);
rawLast = await hutaoService.GetAvatarUsageRanksAsync(true).ConfigureAwait(false);
}
AvatarUsageRanks = avatarUsageRanksRaw.SortByDescending(r => r.Floor).SelectList(rank => new AvatarRankView
AvatarUsageRanks = CurrentLeftJoinLast(raw.SortByDescending(r => r.Floor), rawLast, data => data.Floor, (raw, rawLast) => new AvatarRankView
{
Floor = SH.FormatModelBindingHutaoComplexRankFloor(rank.Floor),
Avatars = rank.Ranks.SortByDescending(r => r.Rate).SelectList(rank => new AvatarView(idAvatarMap[rank.Item], rank.Rate)),
});
Floor = SH.FormatModelBindingHutaoComplexRankFloor(raw.Floor),
Avatars = CurrentLeftJoinLast(raw.Ranks.SortByDescending(r => r.Rate), rawLast?.Ranks, data => data.Item, (rank, rankLast) => new AvatarView(idAvatarMap[rank.Item], rank.Rate, rankLast?.Rate)).ToList(),
}).ToList();
}
[SuppressMessage("", "SH003")]
private async Task AvatarConstellationInfosAsync(Dictionary<AvatarId, Avatar> idAvatarMap)
{
List<AvatarConstellationInfo> avatarConstellationInfosRaw;
List<AvatarConstellationInfo> raw, rawLast;
using (IServiceScope scope = serviceProvider.CreateScope())
{
IHutaoSpiralAbyssService hutaoService = scope.ServiceProvider.GetRequiredService<IHutaoSpiralAbyssService>();
avatarConstellationInfosRaw = await hutaoService.GetAvatarConstellationInfosAsync().ConfigureAwait(false);
raw = await hutaoService.GetAvatarConstellationInfosAsync(false).ConfigureAwait(false);
rawLast = await hutaoService.GetAvatarConstellationInfosAsync(true).ConfigureAwait(false);
}
AvatarConstellationInfos = avatarConstellationInfosRaw.SortBy(i => i.HoldingRate).SelectList(info =>
AvatarConstellationInfos = CurrentLeftJoinLast(raw.SortBy(i => i.HoldingRate), rawLast, data => data.AvatarId, (raw, rawLast) => new AvatarConstellationInfoView(idAvatarMap[raw.AvatarId], raw.HoldingRate, rawLast?.HoldingRate)
{
return new AvatarConstellationInfoView(idAvatarMap[info.AvatarId], info.HoldingRate, info.Constellations.SelectList(x => x.Rate));
});
Rates = CurrentLeftJoinLast(raw.Constellations, rawLast?.Constellations, data => data.Item, (rate, rataLast) => new RateAndDelta(rate.Rate, rataLast?.Rate)).ToList(),
}).ToList();
}
[SuppressMessage("", "SH003")]

View File

@@ -11,45 +11,17 @@ namespace Snap.Hutao.Service.Hutao;
[HighQuality]
internal interface IHutaoSpiralAbyssService
{
/// <summary>
/// 异步获取角色上场率
/// </summary>
/// <returns>角色上场率</returns>
ValueTask<List<AvatarAppearanceRank>> GetAvatarAppearanceRanksAsync();
ValueTask<List<AvatarAppearanceRank>> GetAvatarAppearanceRanksAsync(bool last = false);
/// <summary>
/// 异步获取角色搭配
/// </summary>
/// <returns>角色搭配</returns>
ValueTask<List<AvatarCollocation>> GetAvatarCollocationsAsync();
ValueTask<List<AvatarCollocation>> GetAvatarCollocationsAsync(bool last = false);
/// <summary>
/// 异步获取角色持有率信息
/// </summary>
/// <returns>角色持有率信息</returns>
ValueTask<List<AvatarConstellationInfo>> GetAvatarConstellationInfosAsync();
ValueTask<List<AvatarConstellationInfo>> GetAvatarConstellationInfosAsync(bool last = false);
/// <summary>
/// 异步获取角色使用率
/// </summary>
/// <returns>角色使用率</returns>
ValueTask<List<AvatarUsageRank>> GetAvatarUsageRanksAsync();
ValueTask<List<AvatarUsageRank>> GetAvatarUsageRanksAsync(bool last = false);
/// <summary>
/// 异步获取统计数据
/// </summary>
/// <returns>统计数据</returns>
ValueTask<Overview> GetOverviewAsync();
ValueTask<Overview> GetOverviewAsync(bool last = false);
/// <summary>
/// 异步获取队伍上场
/// </summary>
/// <returns>队伍上场</returns>
ValueTask<List<TeamAppearance>> GetTeamAppearancesAsync();
ValueTask<List<TeamAppearance>> GetTeamAppearancesAsync(bool last = false);
/// <summary>
/// 异步获取武器搭配
/// </summary>
/// <returns>武器搭配</returns>
ValueTask<List<WeaponCollocation>> GetWeaponCollocationsAsync();
ValueTask<List<WeaponCollocation>> GetWeaponCollocationsAsync(bool last = false);
}

View File

@@ -2,16 +2,21 @@
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.Inventory;
internal interface IInventoryDbService
internal interface IInventoryDbService : IAppDbService<InventoryItem>
{
ValueTask AddInventoryItemRangeByProjectId(List<InventoryItem> items);
ValueTask AddInventoryItemRangeByProjectIdAsync(List<InventoryItem> items, CancellationToken token = default);
ValueTask RemoveInventoryItemRangeByProjectId(Guid projectId);
ValueTask RemoveInventoryItemRangeByProjectIdAsync(Guid projectId, CancellationToken token = default);
void UpdateInventoryItem(InventoryItem item);
ValueTask UpdateInventoryItemAsync(InventoryItem item);
ValueTask UpdateInventoryItemAsync(InventoryItem item, CancellationToken token = default);
List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId);
ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default);
}

View File

@@ -1,8 +1,17 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Service.Cultivation;
using Snap.Hutao.ViewModel.Cultivation;
namespace Snap.Hutao.Service.Inventory;
internal interface IInventoryService
{
List<InventoryItemView> GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand);
void SaveInventoryItem(InventoryItemView item);
ValueTask RefreshInventoryAsync(CultivateProject project);
}

View File

@@ -1,10 +1,8 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
using Snap.Hutao.Service.Abstraction;
namespace Snap.Hutao.Service.Inventory;
@@ -14,43 +12,35 @@ internal sealed partial class InventoryDbService : IInventoryDbService
{
private readonly IServiceProvider serviceProvider;
public async ValueTask RemoveInventoryItemRangeByProjectId(Guid projectId)
public IServiceProvider ServiceProvider { get => serviceProvider; }
public async ValueTask RemoveInventoryItemRangeByProjectIdAsync(Guid projectId, CancellationToken token = default)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.InventoryItems
.AsNoTracking()
.Where(a => a.ProjectId == projectId && a.ItemId != 202U) // 摩拉
.ExecuteDeleteAsync()
.ConfigureAwait(false);
}
await this.DeleteAsync(i => i.ProjectId == projectId, token).ConfigureAwait(false);
}
public async ValueTask AddInventoryItemRangeByProjectId(List<InventoryItem> items)
public async ValueTask AddInventoryItemRangeByProjectIdAsync(List<InventoryItem> items, CancellationToken token = default)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.InventoryItems.AddRangeAndSaveAsync(items).ConfigureAwait(false);
}
await this.AddRangeAsync(items, token).ConfigureAwait(false);
}
public void UpdateInventoryItem(InventoryItem item)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
appDbContext.InventoryItems.UpdateAndSave(item);
}
this.Update(item);
}
public async ValueTask UpdateInventoryItemAsync(InventoryItem item)
public async ValueTask UpdateInventoryItemAsync(InventoryItem item, CancellationToken token = default)
{
using (IServiceScope scope = serviceProvider.CreateScope())
{
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.InventoryItems.UpdateAndSaveAsync(item).ConfigureAwait(false);
}
await this.UpdateAsync(item, token).ConfigureAwait(false);
}
public List<InventoryItem> GetInventoryItemListByProjectId(Guid projectId)
{
return this.List(i => i.ProjectId == projectId);
}
public ValueTask<List<InventoryItem>> GetInventoryItemListByProjectIdAsync(Guid projectId, CancellationToken token = default)
{
return this.ListAsync(i => i.ProjectId == projectId, token);
}
}

View File

@@ -1,9 +1,83 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Metadata.Item;
using Snap.Hutao.Service.Cultivation;
using Snap.Hutao.Service.Metadata.ContextAbstraction;
using Snap.Hutao.Service.Notification;
using Snap.Hutao.Service.User;
using Snap.Hutao.ViewModel.Cultivation;
using Snap.Hutao.ViewModel.User;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Service.Inventory;
[Injection(InjectAs.Transient)]
internal sealed class InventoryService : IInventoryService
[ConstructorGenerated]
[Injection(InjectAs.Singleton, typeof(IInventoryService))]
internal sealed partial class InventoryService : IInventoryService
{
private readonly MinimalPromotionDelta minimalPromotionDelta;
private readonly IServiceScopeFactory serviceScopeFactory;
private readonly IInventoryDbService inventoryDbService;
private readonly IInfoBarService infoBarService;
private readonly IUserService userService;
/// <inheritdoc/>
public List<InventoryItemView> GetInventoryItemViews(CultivateProject cultivateProject, ICultivationMetadataContext context, ICommand saveCommand)
{
Guid projectId = cultivateProject.InnerId;
List<InventoryItem> entities = inventoryDbService.GetInventoryItemListByProjectId(projectId);
List<InventoryItemView> results = [];
foreach (Material meta in context.EnumerateInventoryMaterial())
{
InventoryItem entity = entities.SingleOrDefault(e => e.ItemId == meta.Id) ?? InventoryItem.From(projectId, meta.Id);
results.Add(new(entity, meta, saveCommand));
}
return results;
}
/// <inheritdoc/>
public void SaveInventoryItem(InventoryItemView item)
{
inventoryDbService.UpdateInventoryItem(item.Entity);
}
/// <inheritdoc/>
public async ValueTask RefreshInventoryAsync(CultivateProject project)
{
List<AvatarPromotionDelta> deltas = await minimalPromotionDelta.GetAsync().ConfigureAwait(false);
BatchConsumption? batchConsumption = default;
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
if (!UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
infoBarService.Warning(SH.MustSelectUserAndUid);
return;
}
CalculateClient calculateClient = scope.ServiceProvider.GetRequiredService<CalculateClient>();
Response<BatchConsumption>? resp = await calculateClient
.BatchComputeAsync(userAndUid, deltas, true)
.ConfigureAwait(false);
if (!resp.IsOk())
{
return;
}
batchConsumption = resp.Data;
}
if (batchConsumption is { OverallConsume: { } items })
{
await inventoryDbService.RemoveInventoryItemRangeByProjectIdAsync(project.InnerId).ConfigureAwait(false);
await inventoryDbService.AddInventoryItemRangeByProjectIdAsync(items.SelectList(item => InventoryItem.From(project.InnerId, item.Id, (uint)((int)item.Num - item.LackNum)))).ConfigureAwait(false);
}
}
}

View File

@@ -0,0 +1,165 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Google.OrTools.LinearSolver;
using Microsoft.Extensions.Caching.Memory;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Model.Metadata.Abstraction;
using Snap.Hutao.Model.Primitive;
using Snap.Hutao.Service.Metadata;
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
using System.Runtime.InteropServices;
using MetadataAvatar = Snap.Hutao.Model.Metadata.Avatar.Avatar;
using MetadataWeapon = Snap.Hutao.Model.Metadata.Weapon.Weapon;
namespace Snap.Hutao.Service.Inventory;
[ConstructorGenerated]
[Injection(InjectAs.Singleton)]
internal sealed partial class MinimalPromotionDelta
{
private const string CacheKey = $"{nameof(MinimalPromotionDelta)}.Cache";
private readonly ILogger<MinimalPromotionDelta> logger;
private readonly IMetadataService metadataService;
private readonly IMemoryCache memoryCache;
public async ValueTask<List<AvatarPromotionDelta>> GetAsync()
{
if (memoryCache.TryGetRequiredValue(CacheKey, out List<AvatarPromotionDelta>? cache))
{
return cache;
}
List<ICultivationItemsAccess> cultivationItemsEntryList =
[
.. (await metadataService.GetAvatarListAsync().ConfigureAwait(false)).Where(a => a.BeginTime <= DateTimeOffset.Now),
.. (await metadataService.GetWeaponListAsync().ConfigureAwait(false)).Where(w => w.Quality >= Model.Intrinsic.QualityType.QUALITY_BLUE),
];
List<ICultivationItemsAccess> minimal;
using (ValueStopwatch.MeasureExecution(logger))
{
minimal = Minimize(cultivationItemsEntryList);
}
// Gurantee the order of avatar and weapon
// Make sure weapons can have avatar to attach
minimal.Sort(CultivationItemsAccessComparer.Shared);
return memoryCache.Set(CacheKey, ToPromotionDeltaList(minimal));
}
private static List<ICultivationItemsAccess> Minimize(List<ICultivationItemsAccess> cultivationItems)
{
using (Solver? solver = Solver.CreateSolver("SCIP"))
{
ArgumentNullException.ThrowIfNull(solver);
Objective objective = solver.Objective();
objective.SetMinimization();
Dictionary<ICultivationItemsAccess, Variable> itemVariableMap = [];
foreach (ref readonly ICultivationItemsAccess item in CollectionsMarshal.AsSpan(cultivationItems))
{
Variable variable = solver.MakeBoolVar(item.Name);
itemVariableMap[item] = variable;
objective.SetCoefficient(variable, 1);
}
Dictionary<MaterialId, Constraint> materialConstraintMap = [];
foreach (ref readonly ICultivationItemsAccess item in CollectionsMarshal.AsSpan(cultivationItems))
{
foreach (ref readonly MaterialId materialId in CollectionsMarshal.AsSpan(item.CultivationItems))
{
ref Constraint? constraint = ref CollectionsMarshal.GetValueRefOrAddDefault(materialConstraintMap, materialId, out _);
constraint ??= solver.MakeConstraint(1, double.PositiveInfinity, $"{materialId}");
constraint.SetCoefficient(itemVariableMap[item], 1);
}
}
Solver.ResultStatus status = solver.Solve();
HutaoException.ThrowIf(status != Solver.ResultStatus.OPTIMAL, "Unable to solve minimal item set");
List<ICultivationItemsAccess> results = [];
foreach ((ICultivationItemsAccess item, Variable variable) in itemVariableMap)
{
if (variable.SolutionValue() > 0.5)
{
results.Add(item);
}
}
return results;
}
}
private static List<AvatarPromotionDelta> ToPromotionDeltaList(List<ICultivationItemsAccess> cultivationItems)
{
List<AvatarPromotionDelta> deltas = [];
int currentWeaponEmptyAvatarIndex = 0;
foreach (ref readonly ICultivationItemsAccess item in CollectionsMarshal.AsSpan(cultivationItems))
{
switch (item)
{
case MetadataAvatar avatar:
deltas.Add(new()
{
AvatarId = avatar.Id,
AvatarLevelCurrent = 1,
AvatarLevelTarget = 90,
SkillList = avatar.SkillDepot.CompositeSkillsNoInherents().SelectList(skill => new PromotionDelta()
{
Id = skill.GroupId,
LevelCurrent = 1,
LevelTarget = 10,
}),
});
break;
case MetadataWeapon weapon:
AvatarPromotionDelta delta;
if (currentWeaponEmptyAvatarIndex < deltas.Count)
{
delta = deltas[currentWeaponEmptyAvatarIndex++];
}
else
{
delta = new();
deltas.Add(delta);
}
delta.Weapon = new()
{
Id = weapon.Id,
LevelCurrent = 1,
LevelTarget = 90,
};
break;
}
}
return deltas;
}
private sealed class CultivationItemsAccessComparer : IComparer<ICultivationItemsAccess>
{
private static readonly LazySlim<CultivationItemsAccessComparer> LazyShared = new(() => new());
public static CultivationItemsAccessComparer Shared { get => LazyShared.Value; }
public int Compare(ICultivationItemsAccess? x, ICultivationItemsAccess? y)
{
return (x, y) switch
{
(MetadataAvatar, MetadataWeapon) => -1,
(MetadataWeapon, MetadataAvatar) => 1,
_ => 0,
};
}
}
}

View File

@@ -25,8 +25,8 @@ internal sealed partial class DailyNoteRefreshJobScheduler : IJobScheduler
ITrigger dailyNoteTrigger = TriggerBuilder.Create()
.WithIdentity(JobIdentity.DailyNoteRefreshTriggerName, JobIdentity.DailyNoteGroupName)
.StartNow()
.WithSimpleSchedule(builder => builder.WithIntervalInMinutes(interval).RepeatForever())
.WithSimpleSchedule(builder => builder.WithIntervalInSeconds(interval).RepeatForever())
.StartAt(DateTimeOffset.Now.AddSeconds(interval))
.Build();
await scheduler.ScheduleJob(dailyNoteJob, dailyNoteTrigger).ConfigureAwait(false);

View File

@@ -7,5 +7,5 @@ namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataDictionaryIdAchievementSource
{
public Dictionary<AchievementId, Model.Metadata.Achievement.Achievement> IdAchievementMap { get; set; }
Dictionary<AchievementId, Model.Metadata.Achievement.Achievement> IdAchievementMap { get; set; }
}

View File

@@ -7,5 +7,5 @@ namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataDictionaryIdAvatarSource
{
public Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> IdAvatarMap { get; set; }
Dictionary<AvatarId, Model.Metadata.Avatar.Avatar> IdAvatarMap { get; set; }
}

View File

@@ -8,5 +8,5 @@ namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
internal interface IMetadataDictionaryIdMaterialSource
{
public Dictionary<MaterialId, Material> IdMaterialMap { get; set; }
Dictionary<MaterialId, Material> IdMaterialMap { get; set; }
}

Some files were not shown because too many files have changed in this diff Show More