mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
92 Commits
1.9.0
...
feat/launc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76183901da | ||
|
|
87ee81e7fa | ||
|
|
f2f858de15 | ||
|
|
c434521004 | ||
|
|
27ed2cefc1 | ||
|
|
6dc1e664b0 | ||
|
|
51c3dde24b | ||
|
|
2d497faaa5 | ||
|
|
4783934b92 | ||
|
|
03d235876a | ||
|
|
f49e9669af | ||
|
|
533c70caaa | ||
|
|
dd59302bb3 | ||
|
|
96e42f51f0 | ||
|
|
5a19c19759 | ||
|
|
8fb831ef7c | ||
|
|
a30c8d8678 | ||
|
|
2655e835f8 | ||
|
|
ffd74703cd | ||
|
|
584465dc45 | ||
|
|
a1e751160d | ||
|
|
d78d2cf51a | ||
|
|
24709bfbf9 | ||
|
|
9be396b175 | ||
|
|
bb83e76d33 | ||
|
|
1ca24c8a78 | ||
|
|
3d56aef221 | ||
|
|
d43f2e76c4 | ||
|
|
104fb9a3b0 | ||
|
|
d6b79584b6 | ||
|
|
fcd0b65257 | ||
|
|
802951edd7 | ||
|
|
79fc42aa3b | ||
|
|
fb0491dc57 | ||
|
|
b81d088379 | ||
|
|
553d267625 | ||
|
|
199e753103 | ||
|
|
48774960a7 | ||
|
|
7bfea0e090 | ||
|
|
f0f9e387a8 | ||
|
|
f71a34a6be | ||
|
|
e6fd0b833b | ||
|
|
d2c33cf19c | ||
|
|
59a7d6746f | ||
|
|
1d074f5313 | ||
|
|
769a1c3812 | ||
|
|
b54717fa9b | ||
|
|
ffa0b05a12 | ||
|
|
d07a33f3e4 | ||
|
|
b49cd924d0 | ||
|
|
49db3003c9 | ||
|
|
314c771020 | ||
|
|
967f6f76f0 | ||
|
|
5d05c31af5 | ||
|
|
bbd274c391 | ||
|
|
f8a8a929ac | ||
|
|
cf3298dbd0 | ||
|
|
a8b887def2 | ||
|
|
5a937b0838 | ||
|
|
c016ae1cb8 | ||
|
|
c7fdf8001d | ||
|
|
64998453a1 | ||
|
|
9fdedd78d0 | ||
|
|
58e4d1b90e | ||
|
|
e0d11bf9a0 | ||
|
|
51be2c76aa | ||
|
|
686d2378de | ||
|
|
e2d5baffe0 | ||
|
|
4001cc7051 | ||
|
|
b106fe4729 | ||
|
|
d138d856e4 | ||
|
|
91f16c1701 | ||
|
|
54d21b24f7 | ||
|
|
268c2d0543 | ||
|
|
acdcee7558 | ||
|
|
371e469db7 | ||
|
|
22a974408d | ||
|
|
055b343571 | ||
|
|
84e56792b0 | ||
|
|
da95b7837a | ||
|
|
48ddb4c091 | ||
|
|
ea95f2e2b1 | ||
|
|
93077104b8 | ||
|
|
3ffdc901c7 | ||
|
|
0d66c85744 | ||
|
|
b11526761e | ||
|
|
d293149672 | ||
|
|
3784df67a3 | ||
|
|
4aaca4d19f | ||
|
|
e6cf39831d | ||
|
|
24a2a18760 | ||
|
|
d8dce5c062 |
8
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
8
.github/ISSUE_TEMPLATE/MGMT-publish.yml
vendored
@@ -21,17 +21,11 @@ body:
|
||||
- [ ] 更新日志
|
||||
- [ ] 功能文档更新
|
||||
|
||||
## 发布版本 [半自动]
|
||||
|
||||
- [ ] 在 GitHub 个人设置中更新 [Publish-Automate Beta PAT](https://github.com/settings/tokens?type=beta),有效期需小于预计发版需要天数
|
||||
- [ ] 将更新的 PAT 更新至 Publish-Automate 库的 [Actions Secrets](https://github.com/DGP-Automation/Publish-Automate/settings/secrets/actions) 中
|
||||
|
||||
***
|
||||
***
|
||||
|
||||
- [ ] 主分支合并入 release 分支
|
||||
- [ ] 等待 Release 自动发布
|
||||
- [ ] 检查极狐是否同步完成 Release
|
||||
|
||||
- [ ] 通知用户
|
||||
- type: checkboxes
|
||||
id: checklist-final
|
||||
|
||||
1
.github/workflows/alpha.yml
vendored
1
.github/workflows/alpha.yml
vendored
@@ -12,6 +12,7 @@ on:
|
||||
- '.gitmodules'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- '**.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
stages:
|
||||
- fetch
|
||||
- release
|
||||
- refresh
|
||||
|
||||
Fetch:
|
||||
stage: fetch
|
||||
@@ -60,3 +61,13 @@ release:
|
||||
- name: "$THIS_SHA256SUMS_NAME"
|
||||
url: "https://$CI_SERVER_SHELL_SSH_HOST/$CI_PROJECT_PATH/-/jobs/$THIS_JOB_ID/artifacts/raw/$THIS_SHA256SUMS_NAME?inline=false"
|
||||
link_type: other
|
||||
|
||||
Refresh:
|
||||
stage: refresh
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
needs:
|
||||
- job: release
|
||||
script:
|
||||
- apt-get install -y curl
|
||||
- curl -X PATCH "$PURGE_URL"
|
||||
|
||||
@@ -4,13 +4,15 @@
|
||||
|
||||
### Setup Snap.Hutao Project
|
||||
|
||||
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/)
|
||||
2. Open Visual Studio Installer to complete Visual Studio installation
|
||||
- You need to install `.NET desktop development`, `Desktop development with C++` and `Universal Windows Platform development` components
|
||||
3. Install `Single-project MSIX Packaging Tools for VS 2022` provided by Microsoft in Visual Studio marketplace
|
||||
4. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device
|
||||
5. Switch git branch to `develop`
|
||||
6. Open project solution with your Visual Studio and then you are ready to go
|
||||
1. Download and install [Visual Studio 2022 Community](https://visualstudio.microsoft.com/downloads/).
|
||||
- No need to select workloads; Visual Studio will handle it automatically.
|
||||
- Close Visual Studio Installer to ensure a smooth installation experience for workloads.
|
||||
- If using Visual Studio 2022 17.9 preview, skip step 5, as automatic extension installation is supported in this version.
|
||||
2. Use git to clone the project `https://github.com/DGP-Studio/Snap.Hutao.git` to your local device.
|
||||
3. Switch to the`develop` branch using git.
|
||||
4. Open the project solution with your Visual Studio. Visual Studio will prompt you to install the necessary workloads, closing and reopening automatically.
|
||||
5. (For Visual Studio 2022 17.8) Install the [Single-project MSIX Packaging Tools for VS 2022](https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17) provided by Microsoft in Visual Studio marketplace.
|
||||
6. Open the project solution with your Visual Studio, and you are ready to go.
|
||||
|
||||
### Start Pull Request
|
||||
|
||||
|
||||
26
README.md
26
README.md
@@ -1,4 +1,4 @@
|
||||

|
||||

|
||||
|
||||
|
||||
胡桃工具箱是一款以 MIT 协议开源的原神工具箱,专为现代化 Windows 平台设计,旨在改善桌面端玩家的游戏体验。通过将既有的官方资源与开发团队设计的全新 功能相结合,它提供了一套完整且实用的工具集,且无需依赖任何移动设备。它不对游戏客户端进行任何破坏性修改以确保工具箱的安全性
|
||||
@@ -7,7 +7,29 @@ Snap Hutao is an open-source Genshin Impact toolkit under MIT license, designed
|
||||
|
||||
## 下载使用 / Download
|
||||
|
||||
[<img src="https://get.microsoft.com/images/zh-cn%20light.svg" width="30%" height="30%">](https://apps.microsoft.com/store/detail/snap-hutao/9PH4NXJ2JN52)
|
||||
 [](https://github.com/DGP-Studio/Snap.Hutao/releases/latest) []()
|
||||
|
||||
---
|
||||
|
||||
#### 使用安装器安装 / Install with Snap.Hutao.Depolyment Installer
|
||||
|
||||
Snap.Hutao.Depolyment 是一个由 DGP-Studio 重新包装的 Windows 应用安装器,适用于缺少专业计算机知识的一般用户,可以在安装时同时解决缺少必要系统环境的问题。
|
||||
|
||||
Snap.Hutao.Depolyment is a Windows application installer repackaged by DGP-Studio for the users who lacks computer knowledge and can solve the problem of missing necessary system environment at the same time as the installation.
|
||||
|
||||
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao.Deployment/releases/latest)
|
||||
|
||||
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao.Deployment/-/releases)
|
||||
|
||||
#### 使用 MSIX 包安装 / Install with MSIX Package
|
||||
|
||||
直接使用 Snap Hutao MSIX 安装包,使用 Windows 内置的 App Installer 即可安装。如在安装中出现问题,请查阅我们的[常见问题](https://hut.ao/zh/advanced/FAQ.html)文档
|
||||
|
||||
Install with Snap Hutao MSIX package, can be installed with Windows built-in App Installer. If you faced any issue, please check our [FAQ](https://hut.ao/en/advanced/FAQ.html) document.
|
||||
|
||||
[从 GitHub 发布页获取 / Download from GitHub release](https://github.com/DGP-Studio/Snap.Hutao/releases/latest)
|
||||
|
||||
[从极狐Lab 发布页获取 / Download from Jihu Gitlab release](https://jihulab.com/DGP-Studio/Snap.Hutao/-/releases)
|
||||
|
||||
## 贡献 / Contribute
|
||||
|
||||
|
||||
11
src/Snap.Hutao/.vsconfig
Normal file
11
src/Snap.Hutao/.vsconfig
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "1.0",
|
||||
"components": [
|
||||
"Microsoft.VisualStudio.Workload.ManagedDesktop",
|
||||
"Microsoft.VisualStudio.Workload.NativeDesktop",
|
||||
"Microsoft.VisualStudio.Workload.Universal"
|
||||
],
|
||||
"extensions": [
|
||||
"https://marketplace.visualstudio.com/items?itemName=ProjectReunion.MicrosoftSingleProjectMSIXPackagingToolsDev17"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Snap.Hutao.Test.IncomingFeature;
|
||||
|
||||
[TestClass]
|
||||
public class GameRegistryContentTest
|
||||
{
|
||||
private static readonly JsonSerializerOptions RegistryContentSerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
};
|
||||
|
||||
[TestMethod]
|
||||
[SupportedOSPlatform("windows")]
|
||||
public void GetRegistryContent()
|
||||
{
|
||||
GetRegistryContentCore(@"Software\miHoYo\原神");
|
||||
GetRegistryContentCore(@"Software\miHoYo\Genshin Impact");
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static void GetRegistryContentCore(string subkey)
|
||||
{
|
||||
using (RegistryKey key = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Registry64))
|
||||
{
|
||||
RegistryKey? gameKey = key.OpenSubKey(subkey);
|
||||
Assert.IsNotNull(gameKey);
|
||||
|
||||
Dictionary<string, object> data = [];
|
||||
foreach (string valueName in gameKey.GetValueNames())
|
||||
{
|
||||
data[valueName] = gameKey.GetValueKind(valueName) switch
|
||||
{
|
||||
RegistryValueKind.DWord => (int)gameKey.GetValue(valueName)!,
|
||||
RegistryValueKind.Binary => GetStringOrObject((byte[])gameKey.GetValue(valueName)!),
|
||||
_ => throw new NotImplementedException()
|
||||
};
|
||||
}
|
||||
|
||||
Console.WriteLine($"Subkey: {subkey}");
|
||||
Console.WriteLine(JsonSerializer.Serialize(data, RegistryContentSerializerOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe object GetStringOrObject(byte[] bytes)
|
||||
{
|
||||
fixed (byte* pByte = bytes)
|
||||
{
|
||||
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
|
||||
string temp = Encoding.UTF8.GetString(span);
|
||||
|
||||
if (temp.AsSpan()[0] is '{' or '[')
|
||||
{
|
||||
return JsonSerializer.Deserialize<JsonElement>(temp);
|
||||
}
|
||||
|
||||
return temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ FileSaveDialog
|
||||
IFileOpenDialog
|
||||
IFileSaveDialog
|
||||
IPersistFile
|
||||
IShellLinkDataList
|
||||
IShellLinkW
|
||||
ShellLink
|
||||
SHELL_LINK_DATA_FLAGS
|
||||
@@ -69,6 +70,7 @@ SHELL_LINK_DATA_FLAGS
|
||||
IMemoryBufferByteAccess
|
||||
|
||||
// Const value
|
||||
E_FAIL
|
||||
INFINITE
|
||||
RPC_E_WRONG_THREAD
|
||||
MAX_PATH
|
||||
|
||||
@@ -8,6 +8,7 @@ EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9A95A964-04B1-477A-BDE7-505525B3CAD8}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
.vsconfig = .vsconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Snap.Hutao.Test", "Snap.Hutao.Test\Snap.Hutao.Test.csproj", "{D691BA9F-904C-4229-87A5-E14F2EFF2F64}"
|
||||
@@ -87,11 +88,11 @@ Global
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
RESX_AutoApplyExistingTranslations = False
|
||||
RESX_NeutralResourcesLanguage = zh-CN
|
||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||
RESX_SortFileContentOnSave = True
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_Rules = {"EnabledRules":["StringFormat","WhiteSpaceLead","WhiteSpaceTail","PunctuationLead"]}
|
||||
RESX_ShowErrorsInErrorList = False
|
||||
RESX_SortFileContentOnSave = True
|
||||
SolutionGuid = {E4449B1C-0E6A-4D19-955E-1CA491656ABA}
|
||||
RESX_NeutralResourcesLanguage = zh-CN
|
||||
RESX_AutoApplyExistingTranslations = False
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
||||
@@ -69,12 +69,12 @@ public sealed partial class App : Application
|
||||
if (firstInstance.IsCurrent)
|
||||
{
|
||||
logger.LogInformation(ConsoleBanner);
|
||||
LogDiagnosticInformation();
|
||||
|
||||
// manually invoke
|
||||
activation.NonRedirectToActivate(firstInstance, activatedEventArgs);
|
||||
activation.InitializeWith(firstInstance);
|
||||
|
||||
LogDiagnosticInformation();
|
||||
serviceProvider.GetRequiredService<IJumpListInterop>().ConfigureAsync().SafeForget();
|
||||
}
|
||||
else
|
||||
|
||||
@@ -40,12 +40,7 @@ internal sealed class CachedImage : Implementation.ImageEx
|
||||
{
|
||||
// The image is corrupted, remove it.
|
||||
imageCache.Remove(imageUri);
|
||||
return null;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// task was explicitly canceled
|
||||
return null;
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Image
|
||||
Name="PlaceholderImage"
|
||||
Margin="{TemplateBinding PlaceholderMargin}"
|
||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
|
||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"
|
||||
Source="{TemplateBinding PlaceholderSource}"
|
||||
|
||||
@@ -11,10 +11,6 @@ using Windows.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal delegate void ImageExFailedEventHandler(object sender, ImageExFailedEventArgs e);
|
||||
|
||||
internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEventArgs e);
|
||||
|
||||
[SuppressMessage("", "CA1001")]
|
||||
[SuppressMessage("", "SH003")]
|
||||
[TemplateVisualState(Name = LoadingState, GroupName = CommonGroup)]
|
||||
@@ -22,98 +18,34 @@ internal delegate void ImageExOpenedEventHandler(object sender, ImageExOpenedEve
|
||||
[TemplateVisualState(Name = UnloadedState, GroupName = CommonGroup)]
|
||||
[TemplateVisualState(Name = FailedState, GroupName = CommonGroup)]
|
||||
[TemplatePart(Name = PartImage, Type = typeof(object))]
|
||||
internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
|
||||
[TemplatePart(Name = PartPlaceholderImage, Type = typeof(object))]
|
||||
[DependencyProperty("Stretch", typeof(Stretch), Stretch.Uniform)]
|
||||
[DependencyProperty("DecodePixelHeight", typeof(int), 0)]
|
||||
[DependencyProperty("DecodePixelWidth", typeof(int), 0)]
|
||||
[DependencyProperty("DecodePixelType", typeof(DecodePixelType), DecodePixelType.Physical)]
|
||||
[DependencyProperty("IsCacheEnabled", typeof(bool), false)]
|
||||
[DependencyProperty("EnableLazyLoading", typeof(bool), false, nameof(EnableLazyLoadingChanged))]
|
||||
[DependencyProperty("LazyLoadingThreshold", typeof(double), default(double), nameof(LazyLoadingThresholdChanged))]
|
||||
[DependencyProperty("PlaceholderSource", typeof(object), default(object))]
|
||||
[DependencyProperty("PlaceholderStretch", typeof(Stretch), Stretch.Uniform)]
|
||||
[DependencyProperty("PlaceholderMargin", typeof(Thickness))]
|
||||
[DependencyProperty("Source", typeof(object), default(object), nameof(SourceChanged))]
|
||||
internal abstract partial class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlphaMaskProvider
|
||||
{
|
||||
protected const string PartImage = "Image";
|
||||
protected const string PartPlaceholderImage = "PlaceholderImage";
|
||||
protected const string CommonGroup = "CommonStates";
|
||||
protected const string LoadingState = "Loading";
|
||||
protected const string LoadedState = "Loaded";
|
||||
protected const string UnloadedState = "Unloaded";
|
||||
protected const string FailedState = "Failed";
|
||||
|
||||
private static readonly DependencyProperty StretchProperty = DependencyProperty.Register(nameof(Stretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(Stretch.Uniform));
|
||||
private static readonly DependencyProperty DecodePixelHeightProperty = DependencyProperty.Register(nameof(DecodePixelHeight), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
|
||||
private static readonly DependencyProperty DecodePixelTypeProperty = DependencyProperty.Register(nameof(DecodePixelType), typeof(int), typeof(ImageExBase), new PropertyMetadata(DecodePixelType.Physical));
|
||||
private static readonly DependencyProperty DecodePixelWidthProperty = DependencyProperty.Register(nameof(DecodePixelWidth), typeof(int), typeof(ImageExBase), new PropertyMetadata(0));
|
||||
private static readonly DependencyProperty IsCacheEnabledProperty = DependencyProperty.Register(nameof(IsCacheEnabled), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false));
|
||||
private static readonly DependencyProperty EnableLazyLoadingProperty = DependencyProperty.Register(nameof(EnableLazyLoading), typeof(bool), typeof(ImageExBase), new PropertyMetadata(false, EnableLazyLoadingChanged));
|
||||
private static readonly DependencyProperty LazyLoadingThresholdProperty = DependencyProperty.Register(nameof(LazyLoadingThreshold), typeof(double), typeof(ImageExBase), new PropertyMetadata(default(double), LazyLoadingThresholdChanged));
|
||||
private static readonly DependencyProperty PlaceholderSourceProperty = DependencyProperty.Register(nameof(PlaceholderSource), typeof(ImageSource), typeof(ImageExBase), new PropertyMetadata(default(ImageSource), PlaceholderSourceChanged));
|
||||
private static readonly DependencyProperty PlaceholderStretchProperty = DependencyProperty.Register(nameof(PlaceholderStretch), typeof(Stretch), typeof(ImageExBase), new PropertyMetadata(default(Stretch)));
|
||||
private static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(ImageExBase), new PropertyMetadata(null, SourceChanged));
|
||||
|
||||
private CancellationTokenSource? tokenSource;
|
||||
private object? lazyLoadingSource;
|
||||
private bool isInViewport;
|
||||
|
||||
public event ImageExFailedEventHandler? ImageExFailed;
|
||||
|
||||
public event ImageExOpenedEventHandler? ImageExOpened;
|
||||
|
||||
public event EventHandler? ImageExInitialized;
|
||||
|
||||
public bool IsInitialized { get; private set; }
|
||||
|
||||
public int DecodePixelHeight
|
||||
{
|
||||
get => (int)GetValue(DecodePixelHeightProperty);
|
||||
set => SetValue(DecodePixelHeightProperty, value);
|
||||
}
|
||||
|
||||
public DecodePixelType DecodePixelType
|
||||
{
|
||||
get => (DecodePixelType)GetValue(DecodePixelTypeProperty);
|
||||
set => SetValue(DecodePixelTypeProperty, value);
|
||||
}
|
||||
|
||||
public int DecodePixelWidth
|
||||
{
|
||||
get => (int)GetValue(DecodePixelWidthProperty);
|
||||
set => SetValue(DecodePixelWidthProperty, value);
|
||||
}
|
||||
|
||||
public Stretch Stretch
|
||||
{
|
||||
get => (Stretch)GetValue(StretchProperty);
|
||||
set => SetValue(StretchProperty, value);
|
||||
}
|
||||
|
||||
public bool IsCacheEnabled
|
||||
{
|
||||
get => (bool)GetValue(IsCacheEnabledProperty);
|
||||
set => SetValue(IsCacheEnabledProperty, value);
|
||||
}
|
||||
|
||||
public bool EnableLazyLoading
|
||||
{
|
||||
get => (bool)GetValue(EnableLazyLoadingProperty);
|
||||
set => SetValue(EnableLazyLoadingProperty, value);
|
||||
}
|
||||
|
||||
public double LazyLoadingThreshold
|
||||
{
|
||||
get => (double)GetValue(LazyLoadingThresholdProperty);
|
||||
set => SetValue(LazyLoadingThresholdProperty, value);
|
||||
}
|
||||
|
||||
public ImageSource PlaceholderSource
|
||||
{
|
||||
get => (ImageSource)GetValue(PlaceholderSourceProperty);
|
||||
set => SetValue(PlaceholderSourceProperty, value);
|
||||
}
|
||||
|
||||
public Stretch PlaceholderStretch
|
||||
{
|
||||
get => (Stretch)GetValue(PlaceholderStretchProperty);
|
||||
set => SetValue(PlaceholderStretchProperty, value);
|
||||
}
|
||||
|
||||
public object Source
|
||||
{
|
||||
get => GetValue(SourceProperty);
|
||||
set => SetValue(SourceProperty, value);
|
||||
}
|
||||
|
||||
public bool WaitUntilLoaded
|
||||
{
|
||||
get => true;
|
||||
@@ -121,11 +53,9 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
|
||||
protected object? Image { get; private set; }
|
||||
|
||||
public abstract CompositionBrush GetAlphaMask();
|
||||
protected object? PlaceholderImage { get; private set; }
|
||||
|
||||
protected virtual void OnPlaceholderSourceChanged(DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
}
|
||||
public abstract CompositionBrush GetAlphaMask();
|
||||
|
||||
protected virtual Task<ImageSource?> ProvideCachedResourceAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
@@ -136,61 +66,11 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
protected virtual void OnImageOpened(object sender, RoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, LoadedState, true);
|
||||
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
|
||||
}
|
||||
|
||||
protected virtual void OnImageFailed(object sender, ExceptionRoutedEventArgs e)
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new FileNotFoundException(e.ErrorMessage)));
|
||||
}
|
||||
|
||||
protected void AttachImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened += handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected void RemoveImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected void AttachImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed += handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected void RemoveImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
@@ -199,11 +79,10 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
RemoveImageFailed(OnImageFailed);
|
||||
|
||||
Image = GetTemplateChild(PartImage);
|
||||
PlaceholderImage = GetTemplateChild(PartPlaceholderImage);
|
||||
|
||||
IsInitialized = true;
|
||||
|
||||
ImageExInitialized?.Invoke(this, EventArgs.Empty);
|
||||
|
||||
if (Source is null || !EnableLazyLoading || isInViewport)
|
||||
{
|
||||
lazyLoadingSource = null;
|
||||
@@ -218,23 +97,73 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
AttachImageFailed(OnImageFailed);
|
||||
|
||||
base.OnApplyTemplate();
|
||||
|
||||
void AttachImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened += handler;
|
||||
}
|
||||
}
|
||||
|
||||
void AttachImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed += handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed += handler;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveImageOpened(RoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageOpened -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageOpened -= handler;
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveImageFailed(ExceptionRoutedEventHandler handler)
|
||||
{
|
||||
if (Image is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.ImageFailed -= handler;
|
||||
}
|
||||
else if (Image is ImageBrush brush)
|
||||
{
|
||||
brush.ImageFailed -= handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnableLazyLoadingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ImageExBase control)
|
||||
if (d is not ImageExBase control)
|
||||
{
|
||||
bool value = (bool)e.NewValue;
|
||||
if (value)
|
||||
{
|
||||
control.LayoutUpdated += control.ImageExBase_LayoutUpdated;
|
||||
return;
|
||||
}
|
||||
|
||||
control.InvalidateLazyLoading();
|
||||
}
|
||||
else
|
||||
{
|
||||
control.LayoutUpdated -= control.ImageExBase_LayoutUpdated;
|
||||
}
|
||||
bool value = (bool)e.NewValue;
|
||||
if (value)
|
||||
{
|
||||
control.LayoutUpdated += control.OnImageExBaseLayoutUpdated;
|
||||
|
||||
control.InvalidateLazyLoading();
|
||||
}
|
||||
else
|
||||
{
|
||||
control.LayoutUpdated -= control.OnImageExBaseLayoutUpdated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,14 +175,6 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
}
|
||||
}
|
||||
|
||||
private static void PlaceholderSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is ImageExBase control)
|
||||
{
|
||||
control.OnPlaceholderSourceChanged(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (d is not ImageExBase control)
|
||||
@@ -261,17 +182,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.OldValue is null || e.NewValue is null || !e.OldValue.Equals(e.NewValue))
|
||||
if (e.OldValue is not null && e.NewValue is not null && e.OldValue.Equals(e.NewValue))
|
||||
{
|
||||
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
|
||||
{
|
||||
control.lazyLoadingSource = null;
|
||||
control.SetSource(e.NewValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.lazyLoadingSource = e.NewValue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.NewValue is null || !control.EnableLazyLoading || control.isInViewport)
|
||||
{
|
||||
control.lazyLoadingSource = null;
|
||||
control.SetSource(e.NewValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
control.lazyLoadingSource = e.NewValue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,11 +224,24 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
else if (source is BitmapSource { PixelHeight: > 0, PixelWidth: > 0 })
|
||||
{
|
||||
VisualStateManager.GoToState(this, LoadedState, true);
|
||||
ImageExOpened?.Invoke(this, new ImageExOpenedEventArgs());
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "IDE0019")]
|
||||
private void AttachPlaceholderSource(ImageSource? source)
|
||||
{
|
||||
// Setting the source at this point should call ImageExOpened/VisualStateManager.GoToState
|
||||
// as we register to both the ImageOpened/ImageFailed events of the underlying control.
|
||||
// We only need to call those methods if we fail in other cases before we get here.
|
||||
if (PlaceholderImage is Microsoft.UI.Xaml.Controls.Image image)
|
||||
{
|
||||
image.Source = source;
|
||||
}
|
||||
else if (PlaceholderImage is ImageBrush brush)
|
||||
{
|
||||
brush.ImageSource = source;
|
||||
}
|
||||
}
|
||||
|
||||
private async void SetSource(object? source)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
@@ -326,22 +262,19 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
|
||||
VisualStateManager.GoToState(this, LoadingState, true);
|
||||
|
||||
ImageSource? imageSource = source as ImageSource;
|
||||
if (imageSource is not null)
|
||||
if (source as ImageSource is { } imageSource)
|
||||
{
|
||||
AttachSource(imageSource);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
Uri? uri = source as Uri;
|
||||
if (uri is null)
|
||||
if (source as Uri is not { } uri)
|
||||
{
|
||||
string? url = source as string ?? source.ToString();
|
||||
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(new UriFormatException("Invalid uri specified.")));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -355,61 +288,131 @@ internal abstract class ImageExBase : Microsoft.UI.Xaml.Controls.Control, IAlpha
|
||||
{
|
||||
await LoadImageAsync(uri, tokenSource.Token).ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetPlaceholderSource(PlaceholderSource);
|
||||
|
||||
if (ex is OperationCanceledException)
|
||||
{
|
||||
// nothing to do as cancellation has been requested.
|
||||
}
|
||||
else
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void SetPlaceholderSource(object? source)
|
||||
{
|
||||
if (!IsInitialized)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
tokenSource?.Cancel();
|
||||
|
||||
tokenSource = new CancellationTokenSource();
|
||||
|
||||
AttachPlaceholderSource(null);
|
||||
|
||||
if (source is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (source as ImageSource is { } imageSource)
|
||||
{
|
||||
AttachPlaceholderSource(imageSource);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (source as Uri is not { } uri)
|
||||
{
|
||||
string? url = source as string ?? source.ToString();
|
||||
if (!Uri.TryCreate(url, UriKind.RelativeOrAbsolute, out uri))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!IsHttpUri(uri) && !uri.IsAbsoluteUri)
|
||||
{
|
||||
uri = new Uri("ms-appx:///" + uri.OriginalString.TrimStart('/'));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (uri is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImageSource? img = await ProvideCachedResourceAsync(uri, tokenSource.Token).ConfigureAwait(true);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachPlaceholderSource(img);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// nothing to do as cancellation has been requested.
|
||||
}
|
||||
catch (Exception e)
|
||||
catch
|
||||
{
|
||||
VisualStateManager.GoToState(this, FailedState, true);
|
||||
ImageExFailed?.Invoke(this, new ImageExFailedEventArgs(e));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadImageAsync(Uri imageUri, CancellationToken token)
|
||||
{
|
||||
if (imageUri is not null)
|
||||
if (imageUri is null)
|
||||
{
|
||||
if (IsCacheEnabled)
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsCacheEnabled)
|
||||
{
|
||||
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
ImageSource? img = await ProvideCachedResourceAsync(imageUri, token).ConfigureAwait(true);
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachSource(img);
|
||||
}
|
||||
}
|
||||
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string source = imageUri.OriginalString;
|
||||
const string base64Head = "base64,";
|
||||
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
|
||||
BitmapImage bitmap = new();
|
||||
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
// Only attach our image if we still have a valid request.
|
||||
AttachSource(img);
|
||||
AttachSource(bitmap);
|
||||
}
|
||||
}
|
||||
else if (string.Equals(imageUri.Scheme, "data", StringComparison.OrdinalIgnoreCase))
|
||||
}
|
||||
else
|
||||
{
|
||||
AttachSource(new BitmapImage(imageUri)
|
||||
{
|
||||
string source = imageUri.OriginalString;
|
||||
const string base64Head = "base64,";
|
||||
int index = source.IndexOf(base64Head, StringComparison.Ordinal);
|
||||
if (index >= 0)
|
||||
{
|
||||
byte[] bytes = Convert.FromBase64String(source[(index + base64Head.Length)..]);
|
||||
BitmapImage bitmap = new();
|
||||
await bitmap.SetSourceAsync(new MemoryStream(bytes).AsRandomAccessStream());
|
||||
|
||||
ArgumentNullException.ThrowIfNull(tokenSource);
|
||||
if (!tokenSource.IsCancellationRequested)
|
||||
{
|
||||
AttachSource(bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AttachSource(new BitmapImage(imageUri)
|
||||
{
|
||||
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
|
||||
});
|
||||
}
|
||||
CreateOptions = BitmapCreateOptions.IgnoreImageCache,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void ImageExBase_LayoutUpdated(object? sender, object e)
|
||||
private void OnImageExBaseLayoutUpdated(object? sender, object e)
|
||||
{
|
||||
InvalidateLazyLoading();
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal sealed class ImageExFailedEventArgs : EventArgs
|
||||
{
|
||||
public ImageExFailedEventArgs(Exception errorException)
|
||||
{
|
||||
ErrorMessage = ErrorException?.Message;
|
||||
ErrorException = errorException;
|
||||
}
|
||||
|
||||
public Exception? ErrorException { get; private set; }
|
||||
|
||||
public string? ErrorMessage { get; private set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Control.Image.Implementation;
|
||||
|
||||
internal sealed class ImageExOpenedEventArgs : EventArgs
|
||||
{
|
||||
}
|
||||
@@ -18,4 +18,10 @@
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="6"/>
|
||||
</Style>
|
||||
<Style
|
||||
x:Key="FlyoutPresenterPadding16And10Style"
|
||||
BasedOn="{StaticResource DefaultFlyoutPresenterStyle}"
|
||||
TargetType="FlyoutPresenter">
|
||||
<Setter Property="Padding" Value="16,10"/>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
<x:String x:Key="UI_EmotionIcon25">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon25.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon71">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon71.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon250">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon250.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon271">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon271.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon272">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon272.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon293">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon293.png</x:String>
|
||||
<x:String x:Key="UI_EmotionIcon445">https://api.snapgenshin.com/static/raw/EmotionIcon/UI_EmotionIcon445.png</x:String>
|
||||
</ResourceDictionary>
|
||||
|
||||
@@ -18,11 +18,11 @@ internal sealed class CommandLineBuilder
|
||||
/// <summary>
|
||||
/// 当符合条件时添加参数
|
||||
/// </summary>
|
||||
/// <param name="name">参数名称</param>
|
||||
/// <param name="condition">条件</param>
|
||||
/// <param name="name">参数名称</param>
|
||||
/// <param name="value">值</param>
|
||||
/// <returns>命令行建造器</returns>
|
||||
public CommandLineBuilder AppendIf(string name, bool condition, object? value = null)
|
||||
public CommandLineBuilder AppendIf(bool condition, string name, object? value = null)
|
||||
{
|
||||
return condition ? Append(name, value) : this;
|
||||
}
|
||||
@@ -35,7 +35,7 @@ internal sealed class CommandLineBuilder
|
||||
/// <returns>命令行建造器</returns>
|
||||
public CommandLineBuilder AppendIfNotNull(string name, object? value = null)
|
||||
{
|
||||
return AppendIf(name, value is not null, value);
|
||||
return AppendIf(value is not null, name, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此特性附加的属性会在属性改变后会异步地设置的其他属性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
|
||||
internal sealed class AlsoAsyncSetsAttribute : Attribute
|
||||
{
|
||||
public AlsoAsyncSetsAttribute(string propertyName)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoAsyncSetsAttribute(string propertyName1, string propertyName2)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoAsyncSetsAttribute(params string[] propertyNames)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Diagnostics.CodeAnalysis;
|
||||
|
||||
/// <summary>
|
||||
/// 指示此特性附加的属性会在属性改变后会设置的其他属性
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Property, Inherited = false)]
|
||||
internal sealed class AlsoSetsAttribute : Attribute
|
||||
{
|
||||
public AlsoSetsAttribute(string propertyName)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoSetsAttribute(string propertyName1, string propertyName2)
|
||||
{
|
||||
}
|
||||
|
||||
public AlsoSetsAttribute(params string[] propertyNames)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
internal sealed class HutaoException : Exception
|
||||
{
|
||||
public HutaoException(HutaoExceptionKind kind, string message, Exception? innerException)
|
||||
: this(message, innerException)
|
||||
{
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
public HutaoException(string message, Exception? innerException)
|
||||
: base($"{message}\n{innerException?.Message}", innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public HutaoExceptionKind Kind { get; private set; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
internal enum HutaoExceptionKind
|
||||
{
|
||||
None,
|
||||
}
|
||||
@@ -49,6 +49,13 @@ internal static class ThrowHelper
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static NotSupportedException NotSupported(string message)
|
||||
{
|
||||
throw new NotSupportedException(message);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
[MethodImpl(MethodImplOptions.NoInlining)]
|
||||
public static OperationCanceledException OperationCanceled(string message, Exception? inner = default)
|
||||
|
||||
@@ -190,7 +190,7 @@ internal sealed partial class Activation : IActivation
|
||||
|
||||
serviceProvider
|
||||
.GetRequiredService<IDiscordService>()
|
||||
.SetNormalActivity()
|
||||
.SetNormalActivityAsync()
|
||||
.SafeForget();
|
||||
}
|
||||
|
||||
|
||||
21
src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptionsExtension.cs
Normal file
21
src/Snap.Hutao/Snap.Hutao/Core/RuntimeOptionsExtension.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
internal static class RuntimeOptionsExtension
|
||||
{
|
||||
public static string GetDataFolderUpdateCacheFolderFile(this RuntimeOptions options, string fileName)
|
||||
{
|
||||
string directory = Path.Combine(options.DataFolder, "UpdateCache");
|
||||
Directory.CreateDirectory(directory);
|
||||
return Path.Combine(directory, fileName);
|
||||
}
|
||||
|
||||
public static string GetDataFolderServerCacheFolder(this RuntimeOptions options)
|
||||
{
|
||||
return Path.Combine(options.DataFolder, "ServerCache");
|
||||
}
|
||||
}
|
||||
@@ -7,30 +7,30 @@ namespace Snap.Hutao.Core.Setting;
|
||||
/// 设置键
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[SuppressMessage("", "SA1124")]
|
||||
internal static class SettingKeys
|
||||
{
|
||||
#region MainWindow
|
||||
public const string WindowRect = "WindowRect";
|
||||
|
||||
public const string IsNavPaneOpen = "IsNavPaneOpen";
|
||||
|
||||
public const string LaunchTimes = "LaunchTimes";
|
||||
|
||||
public const string DataFolderPath = "DataFolderPath";
|
||||
|
||||
public const string PassportUserName = "PassportUserName";
|
||||
|
||||
public const string PassportPassword = "PassportPassword";
|
||||
|
||||
public const string IsInfoBarToggleChecked = "IsInfoBarToggleChecked";
|
||||
|
||||
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
|
||||
|
||||
public const string ExcludedAnnouncementIds = "ExcludedAnnouncementIds";
|
||||
#endregion
|
||||
|
||||
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
|
||||
#region Application
|
||||
public const string LaunchTimes = "LaunchTimes";
|
||||
public const string DataFolderPath = "DataFolderPath";
|
||||
public const string Major1Minor7Revision0GuideState = "Major1Minor7Revision0GuideState";
|
||||
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
|
||||
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
|
||||
#endregion
|
||||
|
||||
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
|
||||
#region Passport
|
||||
public const string PassportUserName = "PassportUserName";
|
||||
public const string PassportPassword = "PassportPassword";
|
||||
#endregion
|
||||
|
||||
#region Cultivation
|
||||
public const string CultivationAvatarLevelCurrent = "CultivationAvatarLevelCurrent";
|
||||
public const string CultivationAvatarLevelTarget = "CultivationAvatarLevelTarget";
|
||||
public const string CultivationAvatarSkillACurrent = "CultivationAvatarSkillACurrent";
|
||||
@@ -43,13 +43,18 @@ internal static class SettingKeys
|
||||
public const string CultivationWeapon90LevelTarget = "CultivationWeapon90LevelTarget";
|
||||
public const string CultivationWeapon70LevelCurrent = "CultivationWeapon70LevelCurrent";
|
||||
public const string CultivationWeapon70LevelTarget = "CultivationWeapon70LevelTarget";
|
||||
#endregion
|
||||
|
||||
#region HomeCard Dashboard
|
||||
public const string IsHomeCardLaunchGamePresented = "IsHomeCardLaunchGamePresented";
|
||||
public const string IsHomeCardGachaStatisticsPresented = "IsHomeCardGachaStatisticsPresented";
|
||||
public const string IsHomeCardAchievementPresented = "IsHomeCardAchievementPresented";
|
||||
public const string IsHomeCardDailyNotePresented = "IsHomeCardDailyNotePresented";
|
||||
#endregion
|
||||
|
||||
public const string HotKeyMouseClickRepeatForever = "HotKeyMouseClickRepeatForever";
|
||||
|
||||
public const string IsAllocConsoleDebugModeEnabled = "IsAllocConsoleDebugModeEnabled";
|
||||
#region DevTool
|
||||
public const string SuppressMetadataInitialization = "SuppressMetadataInitialization";
|
||||
public const string OverrideElevationRequirement = "OverrideElevationRequirement";
|
||||
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
|
||||
#endregion
|
||||
}
|
||||
@@ -39,6 +39,11 @@ internal sealed class ScheduleTaskInterop : IScheduleTaskInterop
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (WScriptExists(DailyNoteRefreshScriptName, out string fullPath))
|
||||
{
|
||||
File.Delete(fullPath);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Storage;
|
||||
@@ -19,23 +18,16 @@ namespace Snap.Hutao.Core.Shell;
|
||||
internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
||||
{
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
private readonly AppOptions appOptions;
|
||||
|
||||
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
|
||||
{
|
||||
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
|
||||
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
|
||||
|
||||
try
|
||||
{
|
||||
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
|
||||
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
|
||||
using (Stream inputStream = (await iconFile.OpenReadAsync()).AsStream())
|
||||
{
|
||||
using (FileStream outputStream = File.Create(targetLogoPath))
|
||||
{
|
||||
await inputStream.CopyToAsync(outputStream).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -45,12 +37,15 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
||||
HRESULT result = CoCreateInstance<ShellLink, IShellLinkW>(null, CLSCTX.CLSCTX_INPROC_SERVER, out IShellLinkW shellLink);
|
||||
Marshal.ThrowExceptionForHR(result);
|
||||
|
||||
shellLink.SetPath(appOptions.PowerShellPath);
|
||||
shellLink.SetArguments($"""
|
||||
-Command "Start-Process shell:AppsFolder\{runtimeOptions.FamilyName}!App -verb runas"
|
||||
""");
|
||||
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_SHOWMINNOACTIVE);
|
||||
shellLink.SetPath($"shell:AppsFolder\\{runtimeOptions.FamilyName}!App");
|
||||
shellLink.SetShowCmd(SHOW_WINDOW_CMD.SW_NORMAL);
|
||||
shellLink.SetIconLocation(targetLogoPath, 0);
|
||||
|
||||
IShellLinkDataList shellLinkDataList = (IShellLinkDataList)shellLink;
|
||||
shellLinkDataList.GetFlags(out uint flags);
|
||||
flags |= (uint)SHELL_LINK_DATA_FLAGS.SLDF_RUNAS_USER;
|
||||
shellLinkDataList.SetFlags(flags);
|
||||
|
||||
string desktop = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
string target = Path.Combine(desktop, $"{SH.FormatAppNameAndVersion(runtimeOptions.Version)}.lnk");
|
||||
|
||||
|
||||
@@ -1,45 +1,24 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
internal class DispatcherQueueProgress<T> : IProgress<T>
|
||||
{
|
||||
private readonly SynchronizationContext synchronizationContext;
|
||||
private readonly Action<T>? handler;
|
||||
private readonly SendOrPostCallback invokeHandlers;
|
||||
private readonly DispatcherQueue dispatcherQueue;
|
||||
private readonly Action<T> handler;
|
||||
|
||||
public DispatcherQueueProgress(Action<T> handler, SynchronizationContext synchronizationContext)
|
||||
public DispatcherQueueProgress(Action<T> handler, DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
this.synchronizationContext = synchronizationContext;
|
||||
invokeHandlers = new SendOrPostCallback(InvokeHandlers);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(handler);
|
||||
|
||||
this.dispatcherQueue = dispatcherQueue;
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public event EventHandler<T>? ProgressChanged;
|
||||
|
||||
public void Report(T value)
|
||||
{
|
||||
Action<T>? handler = this.handler;
|
||||
EventHandler<T>? changedEvent = ProgressChanged;
|
||||
if (handler is not null || changedEvent is not null)
|
||||
{
|
||||
synchronizationContext.Post(invokeHandlers, value);
|
||||
}
|
||||
}
|
||||
|
||||
[SuppressMessage("", "SH007")]
|
||||
private void InvokeHandlers(object? state)
|
||||
{
|
||||
T value = (T)state!;
|
||||
|
||||
Action<T>? handler = this.handler;
|
||||
EventHandler<T>? changedEvent = ProgressChanged;
|
||||
|
||||
handler?.Invoke(value);
|
||||
changedEvent?.Invoke(this, value);
|
||||
Action<T> handler = this.handler;
|
||||
dispatcherQueue.TryEnqueue(() => handler(value));
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ namespace Snap.Hutao.Core.Threading;
|
||||
/// </summary>
|
||||
internal interface ITaskContext
|
||||
{
|
||||
SynchronizationContext SynchronizationContext { get; }
|
||||
|
||||
void BeginInvokeOnMainThread(Action action);
|
||||
|
||||
void InvokeOnMainThread(Action action);
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
internal interface ITaskContextUnsafe
|
||||
{
|
||||
DispatcherQueue DispatcherQueue { get; }
|
||||
}
|
||||
18
src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs
Normal file
18
src/Snap.Hutao/Snap.Hutao/Core/Threading/SpinWaitPolyfill.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
internal delegate bool SpinWaitPredicate<T>(ref readonly T state);
|
||||
|
||||
internal static class SpinWaitPolyfill
|
||||
{
|
||||
public static unsafe void SpinUntil<T>(ref T state, delegate*<ref readonly T, bool> condition)
|
||||
{
|
||||
SpinWait spinner = default;
|
||||
while (!condition(ref state))
|
||||
{
|
||||
spinner.SpinOnce();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace Snap.Hutao.Core.Threading;
|
||||
/// 任务上下文
|
||||
/// </summary>
|
||||
[Injection(InjectAs.Singleton, typeof(ITaskContext))]
|
||||
internal sealed class TaskContext : ITaskContext
|
||||
internal sealed class TaskContext : ITaskContext, ITaskContextUnsafe
|
||||
{
|
||||
private readonly DispatcherQueueSynchronizationContext synchronizationContext;
|
||||
private readonly DispatcherQueue dispatcherQueue;
|
||||
@@ -24,7 +24,7 @@ internal sealed class TaskContext : ITaskContext
|
||||
SynchronizationContext.SetSynchronizationContext(synchronizationContext);
|
||||
}
|
||||
|
||||
public SynchronizationContext SynchronizationContext { get => synchronizationContext; }
|
||||
public DispatcherQueue DispatcherQueue { get => dispatcherQueue; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ThreadPoolSwitchOperation SwitchToBackgroundAsync()
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Core.Threading;
|
||||
|
||||
internal sealed class Throttler
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> methodSemaphoreMap = new();
|
||||
|
||||
public ValueTask<SemaphoreSlimToken> ThrottleAsync(CancellationToken token = default, [CallerMemberName] string callerName = default!, [CallerLineNumber] int callerLine = 0)
|
||||
{
|
||||
string key = $"{callerName}L{callerLine}";
|
||||
SemaphoreSlim semaphore = methodSemaphoreMap.GetOrAdd(key, name => new SemaphoreSlim(1));
|
||||
return semaphore.EnterAsync(token);
|
||||
}
|
||||
}
|
||||
21
src/Snap.Hutao/Snap.Hutao/Extension/StorageFileExtension.cs
Normal file
21
src/Snap.Hutao/Snap.Hutao/Extension/StorageFileExtension.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.IO;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Extension;
|
||||
|
||||
internal static class StorageFileExtension
|
||||
{
|
||||
public static async ValueTask OverwriteCopyAsync(this StorageFile file, string targetFile)
|
||||
{
|
||||
using (Stream outputStream = (await file.OpenReadAsync()).AsStreamForRead())
|
||||
{
|
||||
using (FileStream inputStream = File.Create(targetFile))
|
||||
{
|
||||
await outputStream.CopyToAsync(inputStream).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
|
||||
namespace Snap.Hutao.Factory.Progress;
|
||||
|
||||
[ConstructorGenerated]
|
||||
@@ -11,6 +13,11 @@ internal sealed partial class ProgressFactory : IProgressFactory
|
||||
|
||||
public IProgress<T> CreateForMainThread<T>(Action<T> handler)
|
||||
{
|
||||
return new DispatcherQueueProgress<T>(handler, taskContext.SynchronizationContext);
|
||||
if (taskContext is not ITaskContextUnsafe @unsafe)
|
||||
{
|
||||
throw ThrowHelper.NotSupported();
|
||||
}
|
||||
|
||||
return new DispatcherQueueProgress<T>(handler, @unsafe.DispatcherQueue);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
<ListView
|
||||
Grid.Row="1"
|
||||
ItemsSource="{Binding GameAccounts}"
|
||||
ItemsSource="{Binding GameAccountsView}"
|
||||
SelectedItem="{Binding SelectedGameAccount, Mode=TwoWay}">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml;
|
||||
using Snap.Hutao.Control;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Windows.Foundation;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[Table("game_accounts")]
|
||||
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string>
|
||||
internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount, string, string, SchemeType>
|
||||
{
|
||||
/// <summary>
|
||||
/// 内部Id
|
||||
@@ -40,21 +40,17 @@ internal sealed class GameAccount : ObservableObject, IMappingFrom<GameAccount,
|
||||
|
||||
/// <summary>
|
||||
/// [MIHOYOSDK_ADL_PROD_CN_h3123967166]
|
||||
/// [MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810]
|
||||
/// </summary>
|
||||
public string MihoyoSDK { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的游戏内账号
|
||||
/// </summary>
|
||||
/// <param name="name">名称</param>
|
||||
/// <param name="sdk">sdk</param>
|
||||
/// <returns>游戏内账号</returns>
|
||||
public static GameAccount From(string name, string sdk)
|
||||
public static GameAccount From(string name, string sdk, SchemeType type)
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Name = name,
|
||||
MihoyoSDK = sdk,
|
||||
Type = type,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -9,18 +9,18 @@ namespace Snap.Hutao.Model.Entity.Primitive;
|
||||
[HighQuality]
|
||||
internal enum SchemeType
|
||||
{
|
||||
/// <summary>
|
||||
/// 国际服
|
||||
/// </summary>
|
||||
Hoyoverse,
|
||||
|
||||
/// <summary>
|
||||
/// 国服官服
|
||||
/// </summary>
|
||||
Official,
|
||||
ChineseOfficial,
|
||||
|
||||
/// <summary>
|
||||
/// 国际服
|
||||
/// </summary>
|
||||
Oversea,
|
||||
|
||||
/// <summary>
|
||||
/// 渠道服
|
||||
/// </summary>
|
||||
Bilibili,
|
||||
ChineseBilibili,
|
||||
}
|
||||
@@ -8,125 +8,45 @@ namespace Snap.Hutao.Model.Entity;
|
||||
/// </summary>
|
||||
internal sealed partial class SettingEntry
|
||||
{
|
||||
/// <summary>
|
||||
/// 游戏路径
|
||||
/// </summary>
|
||||
public const string GamePath = "GamePath";
|
||||
|
||||
public const string GamePathEntries = "GamePathEntries";
|
||||
public const string Culture = "Culture";
|
||||
|
||||
/// <summary>
|
||||
/// PowerShell 路径
|
||||
/// </summary>
|
||||
public const string PowerShellPath = "PowerShellPath";
|
||||
|
||||
/// <summary>
|
||||
/// 空的历史记录卡池是否可见
|
||||
/// </summary>
|
||||
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
|
||||
|
||||
/// <summary>
|
||||
/// 窗口背景类型
|
||||
/// </summary>
|
||||
public const string SystemBackdropType = "SystemBackdropType";
|
||||
|
||||
/// <summary>
|
||||
/// 启用高级功能
|
||||
/// </summary>
|
||||
public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled";
|
||||
public const string AnnouncementRegion = "AnnouncementRegion";
|
||||
|
||||
public const string IsEmptyHistoryWishVisible = "IsEmptyHistoryWishVisible";
|
||||
|
||||
public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺刷新时间
|
||||
/// </summary>
|
||||
public const string DailyNoteRefreshSeconds = "DailyNote.RefreshSeconds";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺提醒式通知
|
||||
/// </summary>
|
||||
public const string DailyNoteReminderNotify = "DailyNote.ReminderNotify";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺免打扰模式
|
||||
/// </summary>
|
||||
public const string DailyNoteSilentWhenPlayingGame = "DailyNote.SilentWhenPlayingGame";
|
||||
|
||||
/// <summary>
|
||||
/// 实时便笺 WebhookUrl
|
||||
/// </summary>
|
||||
public const string DailyNoteWebhookUrl = "DailyNote.WebhookUrl";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 总开关
|
||||
/// </summary>
|
||||
public const string IsAdvancedLaunchOptionsEnabled = "IsAdvancedLaunchOptionsEnabled";
|
||||
|
||||
public const string LaunchIsLaunchOptionsEnabled = "Launch.IsLaunchOptionsEnabled";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 独占全屏
|
||||
/// </summary>
|
||||
public const string LaunchIsExclusive = "Launch.IsExclusive";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 全屏
|
||||
/// </summary>
|
||||
public const string LaunchIsFullScreen = "Launch.IsFullScreen";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 无边框
|
||||
/// </summary>
|
||||
public const string LaunchIsBorderless = "Launch.IsBorderless";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 宽度
|
||||
/// </summary>
|
||||
public const string LaunchScreenWidth = "Launch.ScreenWidth";
|
||||
|
||||
public const string LaunchIsScreenWidthEnabled = "Launch.IsScreenWidthEnabled";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 高度
|
||||
/// </summary>
|
||||
public const string LaunchScreenHeight = "Launch.ScreenHeight";
|
||||
|
||||
public const string LaunchIsScreenHeightEnabled = "Launch.IsScreenHeightEnabled";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 解锁帧率
|
||||
/// </summary>
|
||||
public const string LaunchUnlockFps = "Launch.UnlockFps";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 目标帧率
|
||||
/// </summary>
|
||||
public const string LaunchTargetFps = "Launch.TargetFps";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 显示器编号
|
||||
/// </summary>
|
||||
public const string LaunchMonitor = "Launch.Monitor";
|
||||
|
||||
public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
|
||||
|
||||
public const string LaunchIsUseCloudThirdPartyMobile = "Launch.IsUseCloudThirdPartyMobile";
|
||||
|
||||
public const string LaunchIsWindowsHDREnabled = "Launch.IsWindowsHDREnabled";
|
||||
public const string LaunchUseStarwardPlayTimeStatistics = "Launch.UseStarwardPlayTimeStatistics";
|
||||
|
||||
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
|
||||
|
||||
/// <summary>
|
||||
/// 启动游戏 多倍启动
|
||||
/// </summary>
|
||||
[Obsolete("不再支持多开")]
|
||||
public const string MultipleInstances = "Launch.MultipleInstances";
|
||||
|
||||
/// <summary>
|
||||
/// 语言
|
||||
/// </summary>
|
||||
public const string Culture = "Culture";
|
||||
|
||||
/// <summary>
|
||||
/// 自定义极验接口
|
||||
/// </summary>
|
||||
public const string GeetestCustomCompositeUrl = "GeetestCustomCompositeUrl";
|
||||
|
||||
public const string AnnouncementRegion = "AnnouncementRegion";
|
||||
[Obsolete("不再使用 PowerShell")]
|
||||
public const string PowerShellPath = "PowerShellPath";
|
||||
}
|
||||
|
||||
@@ -93,6 +93,10 @@ internal static class AvatarIds
|
||||
public static readonly AvatarId Neuvillette = 10000087;
|
||||
public static readonly AvatarId Charlotte = 10000088;
|
||||
public static readonly AvatarId Furina = 10000089;
|
||||
public static readonly AvatarId Chevreuse = 10000090;
|
||||
public static readonly AvatarId Navia = 10000091;
|
||||
public static readonly AvatarId Gaming = 10000092;
|
||||
public static readonly AvatarId Xianyun = 10000093;
|
||||
|
||||
/// <summary>
|
||||
/// 检查该角色是否为主角
|
||||
|
||||
@@ -4,20 +4,22 @@
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
IgnorableNamespaces="com uap desktop rescap mp">
|
||||
IgnorableNamespaces="com uap desktop desktop6 rescap mp">
|
||||
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutao"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.9.0.0" />
|
||||
Version="1.9.4.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao</DisplayName>
|
||||
<PublisherDisplayName>DGP Studio</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
@@ -64,5 +66,6 @@
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust"/>
|
||||
<rescap:Capability Name="unvirtualizedResources"/>
|
||||
</Capabilities>
|
||||
</Package>
|
||||
|
||||
@@ -4,20 +4,22 @@
|
||||
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
|
||||
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
|
||||
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
|
||||
xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6"
|
||||
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
|
||||
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
|
||||
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
|
||||
IgnorableNamespaces="com uap desktop rescap mp">
|
||||
IgnorableNamespaces="com uap desktop desktop6 rescap mp">
|
||||
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutaoDev"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.9.0.0" />
|
||||
Version="1.9.4.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao Dev</DisplayName>
|
||||
<PublisherDisplayName>DGP Studio</PublisherDisplayName>
|
||||
<Logo>Assets\StoreLogo.png</Logo>
|
||||
<desktop6:RegistryWriteVirtualization>disabled</desktop6:RegistryWriteVirtualization>
|
||||
</Properties>
|
||||
|
||||
<Dependencies>
|
||||
@@ -64,5 +66,6 @@
|
||||
|
||||
<Capabilities>
|
||||
<rescap:Capability Name="runFullTrust"/>
|
||||
<rescap:Capability Name="unvirtualizedResources"/>
|
||||
</Capabilities>
|
||||
</Package>
|
||||
|
||||
@@ -120,6 +120,12 @@
|
||||
<data name="AppDevNameAndVersion" xml:space="preserve">
|
||||
<value>Snap Hutao Dev {0}</value>
|
||||
</data>
|
||||
<data name="AppElevatedDevNameAndVersion" xml:space="preserve">
|
||||
<value>Snap Hutao Dev {0} [Administrator]</value>
|
||||
</data>
|
||||
<data name="AppElevatedNameAndVersion" xml:space="preserve">
|
||||
<value>Snap Hutao {0} [Administrator]</value>
|
||||
</data>
|
||||
<data name="AppName" xml:space="preserve">
|
||||
<value>Snap Hutao</value>
|
||||
</data>
|
||||
@@ -186,9 +192,6 @@
|
||||
<data name="FilePickerImportCommit" xml:space="preserve">
|
||||
<value>Import</value>
|
||||
</data>
|
||||
<data name="FilePickerPowerShellCommit" xml:space="preserve">
|
||||
<value>Select PowerShell</value>
|
||||
</data>
|
||||
<data name="GuideWindowTitle" xml:space="preserve">
|
||||
<value>Welcome to Snap Hutao, Traveler ~</value>
|
||||
</data>
|
||||
@@ -932,9 +935,6 @@
|
||||
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
|
||||
<value>Unable to set registry key without enabling long path</value>
|
||||
</data>
|
||||
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
|
||||
<value>PowerShell installation directory not found</value>
|
||||
</data>
|
||||
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
|
||||
<value>Unable to read game config file {0}, file may be not exist</value>
|
||||
</data>
|
||||
@@ -1553,6 +1553,9 @@
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>Switch game account failed</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
|
||||
<value>Cannot select account [{1}] for Uid [{0}], the account does not belong to current server</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingActionComplete" xml:space="preserve">
|
||||
<value>Operation completed</value>
|
||||
</data>
|
||||
@@ -2144,6 +2147,9 @@
|
||||
<data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve">
|
||||
<value>Pre-download</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSelectGamePath" xml:space="preserve">
|
||||
<value>Select Game Path</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
|
||||
<value>This account has not been bound to the Real-time Note notification UID.</value>
|
||||
</data>
|
||||
@@ -2276,6 +2282,15 @@
|
||||
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
|
||||
<value>Device IP</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeDescription" xml:space="preserve">
|
||||
<value>Administrator mode will change some features' availability and behaviors</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeHeader" xml:space="preserve">
|
||||
<value>Administrator Mode</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeRestartAction" xml:space="preserve">
|
||||
<value>Restart with Administrator Mode</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
|
||||
<value>Show or hide wish event with no wish history</value>
|
||||
</data>
|
||||
@@ -2432,12 +2447,6 @@
|
||||
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
|
||||
<value>When setting the game path, please select the game program (Yuanshen.exe or GenshinImpact.exe) instead of the game launcher (launcher.exe)</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
|
||||
<value>Snap Hutao uses PowerShell to modify information in registry to change game accounts</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
|
||||
<value>PowerShell Path</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
|
||||
<value>Shell Experience</value>
|
||||
</data>
|
||||
@@ -2741,6 +2750,15 @@
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>Copied to clipboard</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusFinished" xml:space="preserve">
|
||||
<value>Completed</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusNotOpen" xml:space="preserve">
|
||||
<value>Locked</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusOngoing" xml:space="preserve">
|
||||
<value>In Progress</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>Finished</value>
|
||||
</data>
|
||||
|
||||
@@ -120,6 +120,12 @@
|
||||
<data name="AppDevNameAndVersion" xml:space="preserve">
|
||||
<value>Snap Hutao Dev {0}</value>
|
||||
</data>
|
||||
<data name="AppElevatedDevNameAndVersion" xml:space="preserve">
|
||||
<value>Snap Hutao Dev {0} [Administrator]</value>
|
||||
</data>
|
||||
<data name="AppElevatedNameAndVersion" xml:space="preserve">
|
||||
<value>Snap Hutao {0} [Administrator]</value>
|
||||
</data>
|
||||
<data name="AppName" xml:space="preserve">
|
||||
<value>Snap Hutao</value>
|
||||
</data>
|
||||
@@ -186,9 +192,6 @@
|
||||
<data name="FilePickerImportCommit" xml:space="preserve">
|
||||
<value>Impor</value>
|
||||
</data>
|
||||
<data name="FilePickerPowerShellCommit" xml:space="preserve">
|
||||
<value>Pilih PowerShell</value>
|
||||
</data>
|
||||
<data name="GuideWindowTitle" xml:space="preserve">
|
||||
<value>Selamat Datang di Snap Hutao, Traveler ~</value>
|
||||
</data>
|
||||
@@ -932,9 +935,6 @@
|
||||
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
|
||||
<value>Tidak dapat mengatur kunci registri tanpa mengaktifkan path panjang</value>
|
||||
</data>
|
||||
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
|
||||
<value>Direktori instalasi PowerShell tidak ditemukan</value>
|
||||
</data>
|
||||
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
|
||||
<value>Tidak dapat membaca file konfigurasi game {0}, file mungkin tidak ada</value>
|
||||
</data>
|
||||
@@ -996,7 +996,7 @@
|
||||
<value>Versi UIGF tidak didukung</value>
|
||||
</data>
|
||||
<data name="ServiceUpdateStatusVersionDescription" xml:space="preserve">
|
||||
<value>发现新版本 {0}</value>
|
||||
<value>Versi terbau {0} sudah tersedia.</value>
|
||||
</data>
|
||||
<data name="ServiceUserCurrentMultiMatched" xml:space="preserve">
|
||||
<value>Beberapa pengguna tercatat sebagai dipilih</value>
|
||||
@@ -1553,6 +1553,9 @@
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>Gagal ganti akun game</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
|
||||
<value>Tidak bisa memilih akun [{1}] dengan UID [{0}], Akun itu tidak berada pada server tersebut</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingActionComplete" xml:space="preserve">
|
||||
<value>Tindakan selesai</value>
|
||||
</data>
|
||||
@@ -2097,7 +2100,7 @@
|
||||
<value>Mengatur Status Aktivitas Discord Saya Ketika Saya Sedang Bermain Game</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameDiscordActivityHeader" xml:space="preserve">
|
||||
<value>Discord Activity</value>
|
||||
<value>Aktifitas Discord</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameFileHeader" xml:space="preserve">
|
||||
<value>Berkas</value>
|
||||
@@ -2144,6 +2147,9 @@
|
||||
<data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve">
|
||||
<value>Prapengunduhan</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSelectGamePath" xml:space="preserve">
|
||||
<value>Pilih Path Game</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
|
||||
<value>Akun ini belum terikat ke UID notifikasi Catatan Realtime.</value>
|
||||
</data>
|
||||
@@ -2276,6 +2282,15 @@
|
||||
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
|
||||
<value>ID Perangkat</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeDescription" xml:space="preserve">
|
||||
<value>Mode administrator akan mengubah ketersediaan dan perilaku beberapa fitur</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeHeader" xml:space="preserve">
|
||||
<value>Mode Admin</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeRestartAction" xml:space="preserve">
|
||||
<value>Restart menggunakan Mode Administrator</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
|
||||
<value>Tampilkan atau sembunyikan acara keinginan tanpa riwayat Wish</value>
|
||||
</data>
|
||||
@@ -2432,12 +2447,6 @@
|
||||
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
|
||||
<value>Saat mengatur jalur permainan, pilih program permainan (Yuanshen.exe atau GenshinImpact.exe) bukan peluncur permainan (launcher.exe)</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
|
||||
<value>Snap Hutao menggunakan PowerShell untuk memodifikasi informasi di registri untuk mengubah akun Game</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
|
||||
<value>Path PowerShell</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
|
||||
<value>Pengalaman Shell</value>
|
||||
</data>
|
||||
@@ -2625,10 +2634,10 @@
|
||||
<value>Mengunggah Data</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageReadyContent" xml:space="preserve">
|
||||
<value>是否立即安装?</value>
|
||||
<value>Pasang sekarang?</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageReadyTitle" xml:space="preserve">
|
||||
<value>胡桃 {0} 版本已准备就绪</value>
|
||||
<value>Snap Hutao versi {0} sudah siap</value>
|
||||
</data>
|
||||
<data name="ViewTitleAutoClicking" xml:space="preserve">
|
||||
<value>Auto Click</value>
|
||||
@@ -2741,6 +2750,15 @@
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>Disalin ke clipboard</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusFinished" xml:space="preserve">
|
||||
<value>Selesai</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusNotOpen" xml:space="preserve">
|
||||
<value>Terkunci</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusOngoing" xml:space="preserve">
|
||||
<value>Dalam proses</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>Selesai</value>
|
||||
</data>
|
||||
@@ -2886,7 +2904,7 @@
|
||||
<value>Server Internasional Server Asia</value>
|
||||
</data>
|
||||
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
|
||||
<value>国际服 港澳台服</value>
|
||||
<value>Server International: Server TW/HK/MU</value>
|
||||
</data>
|
||||
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
|
||||
<value>Server Internasional Server Eropa</value>
|
||||
|
||||
@@ -120,6 +120,12 @@
|
||||
<data name="AppDevNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃 Dev {0}</value>
|
||||
</data>
|
||||
<data name="AppElevatedDevNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃Dev {0} [管理者]</value>
|
||||
</data>
|
||||
<data name="AppElevatedNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃 {0} [管理者]</value>
|
||||
</data>
|
||||
<data name="AppName" xml:space="preserve">
|
||||
<value>胡桃</value>
|
||||
</data>
|
||||
@@ -186,9 +192,6 @@
|
||||
<data name="FilePickerImportCommit" xml:space="preserve">
|
||||
<value>インポート</value>
|
||||
</data>
|
||||
<data name="FilePickerPowerShellCommit" xml:space="preserve">
|
||||
<value>PowerShellを選択</value>
|
||||
</data>
|
||||
<data name="GuideWindowTitle" xml:space="preserve">
|
||||
<value>胡桃へようこそ</value>
|
||||
</data>
|
||||
@@ -932,9 +935,6 @@
|
||||
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
|
||||
<value>長いパスのサポートがオフになっているため、レジストリキーを編集できません。</value>
|
||||
</data>
|
||||
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
|
||||
<value>PowerShellのインストールディレクトリが見つかりません</value>
|
||||
</data>
|
||||
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
|
||||
<value>ゲーム設定ファイル {0} の読み込みに失敗しました。ファイルが存在していない可能性があります。</value>
|
||||
</data>
|
||||
@@ -996,7 +996,7 @@
|
||||
<value>サポートされていないUIGFバージョン</value>
|
||||
</data>
|
||||
<data name="ServiceUpdateStatusVersionDescription" xml:space="preserve">
|
||||
<value>发现新版本 {0}</value>
|
||||
<value>新しいバージョン {0} が利用できます</value>
|
||||
</data>
|
||||
<data name="ServiceUserCurrentMultiMatched" xml:space="preserve">
|
||||
<value>ユーザー情報を複数選択しています。</value>
|
||||
@@ -1553,6 +1553,9 @@
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>アカウントの切り替えができませんでした</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
|
||||
<value>UID [{0}] に対するアカウント [{1}] を選択できませんでした。このアカウントは現在のサーバーに属していません。</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingActionComplete" xml:space="preserve">
|
||||
<value>操作は完了しました</value>
|
||||
</data>
|
||||
@@ -1653,7 +1656,7 @@
|
||||
<value>重要</value>
|
||||
</data>
|
||||
<data name="ViewPageAnnouncementViewDetails" xml:space="preserve">
|
||||
<value>查看详情</value>
|
||||
<value>詳細を表示</value>
|
||||
</data>
|
||||
<data name="ViewPageAvatarPropertyArtifactScore" xml:space="preserve">
|
||||
<value>聖遺物スコア</value>
|
||||
@@ -2144,6 +2147,9 @@
|
||||
<data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve">
|
||||
<value>事前ダウンロード</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSelectGamePath" xml:space="preserve">
|
||||
<value>ゲームのフォルダを選択</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
|
||||
<value>このアカウントはリアルタイムノート通知 UID として連携されていません。</value>
|
||||
</data>
|
||||
@@ -2276,6 +2282,15 @@
|
||||
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
|
||||
<value>デバイスのIP</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeDescription" xml:space="preserve">
|
||||
<value>管理者モードは一部の機能の使用可否と動作に影響します。</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeHeader" xml:space="preserve">
|
||||
<value>管理者モード</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeRestartAction" xml:space="preserve">
|
||||
<value>管理者モードで再起動</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
|
||||
<value>祈願履歴のないイベント限定祈願を祈願履歴に表示するかを変更します。</value>
|
||||
</data>
|
||||
@@ -2313,10 +2328,10 @@
|
||||
<value>CAPTCHA</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHomeAnnouncementRegionDescription" xml:space="preserve">
|
||||
<value>选择想要获取公告的游戏服务器</value>
|
||||
<value>お知らせを受け取りたいサーバーを選択してください</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingHomeAnnouncementRegionHeader" xml:space="preserve">
|
||||
<value>公告所属服务器</value>
|
||||
<value>お知らせのサーバー</value>
|
||||
</data>
|
||||
<data name="ViewpageSettingHomeCardDescription" xml:space="preserve">
|
||||
<value>ダッシュボードを整理する</value>
|
||||
@@ -2432,12 +2447,6 @@
|
||||
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
|
||||
<value>ゲームのパスを設定する際、本体(YuanShen.exe または GenshinImpact.exe)を選んでください。ランチャー(launcher.exe)ではありません</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
|
||||
<value>胡桃のゲームランチャーはPowershellを介してレジストリを変更し、ゲームで使用するアカウントを変更します。</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
|
||||
<value>PowerShell パス</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
|
||||
<value>Shell エクスペリエンス</value>
|
||||
</data>
|
||||
@@ -2625,10 +2634,10 @@
|
||||
<value>データをアップロード</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageReadyContent" xml:space="preserve">
|
||||
<value>是否立即安装?</value>
|
||||
<value>今すぐインストールしますか?</value>
|
||||
</data>
|
||||
<data name="ViewTitileUpdatePackageReadyTitle" xml:space="preserve">
|
||||
<value>胡桃 {0} 版本已准备就绪</value>
|
||||
<value>胡桃 {0} の準備が完了</value>
|
||||
</data>
|
||||
<data name="ViewTitleAutoClicking" xml:space="preserve">
|
||||
<value>オートクリック</value>
|
||||
@@ -2741,6 +2750,15 @@
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>クリップボードにコピーしました。</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusFinished" xml:space="preserve">
|
||||
<value>すべて完了</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusNotOpen" xml:space="preserve">
|
||||
<value>未解放</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusOngoing" xml:space="preserve">
|
||||
<value>進行中</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>完了しました。</value>
|
||||
</data>
|
||||
@@ -2871,28 +2889,28 @@
|
||||
<value>ダウンロードリンクのコピーに成功しました</value>
|
||||
</data>
|
||||
<data name="WebHoyolabInvalidRegion" xml:space="preserve">
|
||||
<value>无效的服务器</value>
|
||||
<value>無効なサーバー</value>
|
||||
</data>
|
||||
<data name="WebHoyolabInvalidUid" xml:space="preserve">
|
||||
<value>無効なUIDです</value>
|
||||
</data>
|
||||
<data name="WebHoyolabRegionCNGF01" xml:space="preserve">
|
||||
<value>国服 官方服</value>
|
||||
<value>中国サーバー: 公式</value>
|
||||
</data>
|
||||
<data name="WebHoyolabRegionCNQD01" xml:space="preserve">
|
||||
<value>国服 渠道服</value>
|
||||
<value>中国サーバー: ビリビリ(bilibili)</value>
|
||||
</data>
|
||||
<data name="WebHoyolabRegionOSASIA" xml:space="preserve">
|
||||
<value>国际服 亚服</value>
|
||||
<value>国内サーバー: アジア</value>
|
||||
</data>
|
||||
<data name="WebHoyolabRegionOSCHT" xml:space="preserve">
|
||||
<value>国际服 港澳台服</value>
|
||||
<value>海外サーバー: 台湾/香港/モンゴル</value>
|
||||
</data>
|
||||
<data name="WebHoyolabRegionOSEURO" xml:space="preserve">
|
||||
<value>国际服 欧服</value>
|
||||
<value>海外サーバー: ヨーロッパ</value>
|
||||
</data>
|
||||
<data name="WebHoyolabRegionOSUSA" xml:space="preserve">
|
||||
<value>国际服 美服</value>
|
||||
<value>海外サーバー: 北アメリカ</value>
|
||||
</data>
|
||||
<data name="WebHutaoServiceUnAvailable" xml:space="preserve">
|
||||
<value>胡桃サーバがメンテナンス中です</value>
|
||||
|
||||
@@ -120,6 +120,12 @@
|
||||
<data name="AppDevNameAndVersion" xml:space="preserve">
|
||||
<value>호두 Dev {0}</value>
|
||||
</data>
|
||||
<data name="AppElevatedDevNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃Dev {0} [管理员]</value>
|
||||
</data>
|
||||
<data name="AppElevatedNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃 {0} [管理员]</value>
|
||||
</data>
|
||||
<data name="AppName" xml:space="preserve">
|
||||
<value>호두</value>
|
||||
</data>
|
||||
@@ -186,9 +192,6 @@
|
||||
<data name="FilePickerImportCommit" xml:space="preserve">
|
||||
<value>가져오기</value>
|
||||
</data>
|
||||
<data name="FilePickerPowerShellCommit" xml:space="preserve">
|
||||
<value>选择 PowerShell</value>
|
||||
</data>
|
||||
<data name="GuideWindowTitle" xml:space="preserve">
|
||||
<value>欢迎使用胡桃</value>
|
||||
</data>
|
||||
@@ -932,9 +935,6 @@
|
||||
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
|
||||
<value>긴 경로 기능이 켜지지 않아 레지스트리 키 값을 설정할 수 없습니다</value>
|
||||
</data>
|
||||
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
|
||||
<value>PowerShell 설치 경로를 찾을 수 없습니다</value>
|
||||
</data>
|
||||
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
|
||||
<value>无法读取游戏配置文件 {0},可能是文件不存在</value>
|
||||
</data>
|
||||
@@ -1553,6 +1553,9 @@
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>계정 전환 살패</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
|
||||
<value>无法选择UID [{0}] 对应的账号 [{1}],该账号不属于当前服务器</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingActionComplete" xml:space="preserve">
|
||||
<value>操作完成</value>
|
||||
</data>
|
||||
@@ -2144,6 +2147,9 @@
|
||||
<data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve">
|
||||
<value>사전 다운로드</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSelectGamePath" xml:space="preserve">
|
||||
<value>选择游戏路径</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
|
||||
<value>该账号尚未绑定实时便笺通知 UID</value>
|
||||
</data>
|
||||
@@ -2276,6 +2282,15 @@
|
||||
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
|
||||
<value>设备 IP</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeDescription" xml:space="preserve">
|
||||
<value>管理员模式会影响部分功能的可用性与行为</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeHeader" xml:space="preserve">
|
||||
<value>管理员模式</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeRestartAction" xml:space="preserve">
|
||||
<value>以管理员身份重启</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
|
||||
<value>기원 기록 페이지에 기록되지 않은 오래된 기원 이벤트를 표시하거나 숨깁니다</value>
|
||||
</data>
|
||||
@@ -2432,12 +2447,6 @@
|
||||
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
|
||||
<value>게임 경로를 설정할 때 런쳐(launcher.exe) 대신 게임(YuanShen.exe 또는 GenshinImpact.exe)를 선택하세요</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
|
||||
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
|
||||
<value>PowerShell 路径</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
|
||||
<value>Shell 体验</value>
|
||||
</data>
|
||||
@@ -2741,6 +2750,15 @@
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>已复制到剪贴板</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusFinished" xml:space="preserve">
|
||||
<value>全部完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusNotOpen" xml:space="preserve">
|
||||
<value>尚未开启</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusOngoing" xml:space="preserve">
|
||||
<value>进行中</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>已完成</value>
|
||||
</data>
|
||||
|
||||
@@ -120,6 +120,12 @@
|
||||
<data name="AppDevNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃 Dev {0}</value>
|
||||
</data>
|
||||
<data name="AppElevatedDevNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃 Dev {0} [管理员]</value>
|
||||
</data>
|
||||
<data name="AppElevatedNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃 {0} [管理员]</value>
|
||||
</data>
|
||||
<data name="AppName" xml:space="preserve">
|
||||
<value>胡桃</value>
|
||||
</data>
|
||||
@@ -186,9 +192,6 @@
|
||||
<data name="FilePickerImportCommit" xml:space="preserve">
|
||||
<value>导入</value>
|
||||
</data>
|
||||
<data name="FilePickerPowerShellCommit" xml:space="preserve">
|
||||
<value>选择 PowerShell</value>
|
||||
</data>
|
||||
<data name="GuideWindowTitle" xml:space="preserve">
|
||||
<value>欢迎使用胡桃</value>
|
||||
</data>
|
||||
@@ -867,11 +870,23 @@
|
||||
<value>文件系统权限不足,无法转换服务器</value>
|
||||
</data>
|
||||
<data name="ServiceGameEnsureGameResourceQueryResourceInformation" xml:space="preserve">
|
||||
<value>查询游戏资源信息</value>
|
||||
<value>下载游戏资源索引</value>
|
||||
</data>
|
||||
<data name="ServiceGameFileOperationExceptionMessage" xml:space="preserve">
|
||||
<value>游戏文件操作失败:{0}</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchExecutionGameFpsUnlockFailed" xml:space="preserve">
|
||||
<value>解锁帧率上限失败</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchExecutionGameIsRunning" xml:space="preserve">
|
||||
<value>游戏进程运行中</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchExecutionGamePathNotValid" xml:space="preserve">
|
||||
<value>请选择游戏路径</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchExecutionGameResourceQueryIndexFailed" xml:space="preserve">
|
||||
<value>下载游戏资源索引失败: {0}</value>
|
||||
</data>
|
||||
<data name="ServiceGameLaunchPhaseProcessExited" xml:space="preserve">
|
||||
<value>游戏进程已退出</value>
|
||||
</data>
|
||||
@@ -932,9 +947,6 @@
|
||||
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
|
||||
<value>未开启长路径功能,无法设置注册表键值</value>
|
||||
</data>
|
||||
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
|
||||
<value>找不到 PowerShell 的安装目录</value>
|
||||
</data>
|
||||
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
|
||||
<value>无法读取游戏配置文件 {0},可能是文件不存在</value>
|
||||
</data>
|
||||
@@ -1271,6 +1283,12 @@
|
||||
<data name="ViewDialogQRCodeTitle" xml:space="preserve">
|
||||
<value>使用米游社扫描二维码</value>
|
||||
</data>
|
||||
<data name="ViewDialogReconfirmTextHeader" xml:space="preserve">
|
||||
<value>请输入你正在启用的功能标题</value>
|
||||
</data>
|
||||
<data name="ViewDialogReconfirmTitle" xml:space="preserve">
|
||||
<value>你正在启用一个危险功能</value>
|
||||
</data>
|
||||
<data name="ViewDialogSettingDeleteUserDataContent" xml:space="preserve">
|
||||
<value>该操作是不可逆的,所有用户登录状态会丢失</value>
|
||||
</data>
|
||||
@@ -1553,6 +1571,9 @@
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>切换账号失败</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
|
||||
<value>无法选择UID [{0}] 对应的账号 [{1}],该账号不属于当前服务器</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingActionComplete" xml:space="preserve">
|
||||
<value>操作完成</value>
|
||||
</data>
|
||||
@@ -1571,6 +1592,12 @@
|
||||
<data name="ViewModelSettingCreateDesktopShortcutFailed" xml:space="preserve">
|
||||
<value>创建桌面快捷方式失败</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingDeleteServerCacheFolderContent" xml:space="preserve">
|
||||
<value>后续转换会重新下载所需的文件,确定要删除吗?</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingDeleteServerCacheFolderTitle" xml:space="preserve">
|
||||
<value>删除转换服务器游戏客户端缓存</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingFolderSizeDescription" xml:space="preserve">
|
||||
<value>已使用磁盘空间:{0}</value>
|
||||
</data>
|
||||
@@ -2144,6 +2171,9 @@
|
||||
<data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve">
|
||||
<value>预下载</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSelectGamePath" xml:space="preserve">
|
||||
<value>选择游戏路径</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
|
||||
<value>该账号尚未绑定实时便笺通知 UID</value>
|
||||
</data>
|
||||
@@ -2186,6 +2216,12 @@
|
||||
<data name="ViewPageLaunchGameUnlockFpsOn" xml:space="preserve">
|
||||
<value>启用</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameWindowsHDRDescription" xml:space="preserve">
|
||||
<value>充分利用支持高动态范围的显示器获得更亮、更生动、更精细的画面</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameWindowsHDRHeader" xml:space="preserve">
|
||||
<value>Windows HDR</value>
|
||||
</data>
|
||||
<data name="ViewPageLoginHoyoverseUserHint" xml:space="preserve">
|
||||
<value>请输入你的 HoYoLab Uid</value>
|
||||
</data>
|
||||
@@ -2232,7 +2268,7 @@
|
||||
<value>创建</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingCreateDesktopShortcutDescription" xml:space="preserve">
|
||||
<value>在桌面上创建默认以管理员方式启动的快捷方式</value>
|
||||
<value>在桌面上创建默认以管理员身份启动的快捷方式</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingCreateDesktopShortcutHeader" xml:space="preserve">
|
||||
<value>创建快捷方式</value>
|
||||
@@ -2276,6 +2312,15 @@
|
||||
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
|
||||
<value>设备 IP</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeDescription" xml:space="preserve">
|
||||
<value>管理员模式会影响部分功能的可用性与行为</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeHeader" xml:space="preserve">
|
||||
<value>管理员模式</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeRestartAction" xml:space="preserve">
|
||||
<value>以管理员身份重启</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
|
||||
<value>在祈愿记录页面显示或隐藏无记录的历史祈愿活动</value>
|
||||
</data>
|
||||
@@ -2394,7 +2439,7 @@
|
||||
<value>在完整阅读原神和胡桃工具箱用户协议后,我选择启用「启动游戏-高级功能」</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingIsAdvancedLaunchOptionsEnabledHeader" xml:space="preserve">
|
||||
<value>启动高级功能</value>
|
||||
<value>高级功能</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingKeyShortcutAutoClickingDescription" xml:space="preserve">
|
||||
<value>更改自动连点功能的快捷键</value>
|
||||
@@ -2432,12 +2477,6 @@
|
||||
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
|
||||
<value>设置游戏路径时,请选择游戏本体(YuanShen.exe 或 GenshinImpact.exe)而不是启动器(launcher.exe)</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
|
||||
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
|
||||
<value>PowerShell 路径</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
|
||||
<value>Shell 体验</value>
|
||||
</data>
|
||||
@@ -2555,6 +2594,12 @@
|
||||
<data name="ViewSettingAllocConsoleHeader" xml:space="preserve">
|
||||
<value>调试控制台</value>
|
||||
</data>
|
||||
<data name="ViewSettingDeleteServerCacheFolderDescription" xml:space="preserve">
|
||||
<value>在启动游戏中转换服务器后会产生对应的游戏客户端文件用作缓存</value>
|
||||
</data>
|
||||
<data name="ViewSettingDeleteServerCacheFolderHeader" xml:space="preserve">
|
||||
<value>删除转换服务器缓存</value>
|
||||
</data>
|
||||
<data name="ViewSettingFolderViewOpenFolderAction" xml:space="preserve">
|
||||
<value>打开文件夹</value>
|
||||
</data>
|
||||
@@ -2738,9 +2783,21 @@
|
||||
<data name="WebAnnouncementTimeHoursEndFormat" xml:space="preserve">
|
||||
<value>{0} 小时后结束</value>
|
||||
</data>
|
||||
<data name="WebBridgeShareCopyToClipboardFailed" xml:space="preserve">
|
||||
<value>打开剪贴板失败</value>
|
||||
</data>
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>已复制到剪贴板</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusFinished" xml:space="preserve">
|
||||
<value>全部完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusNotOpen" xml:space="preserve">
|
||||
<value>尚未开启</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusOngoing" xml:space="preserve">
|
||||
<value>进行中</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>已完成</value>
|
||||
</data>
|
||||
|
||||
@@ -120,11 +120,17 @@
|
||||
<data name="AppDevNameAndVersion" xml:space="preserve">
|
||||
<value>Snap Hutao Dev {0}</value>
|
||||
</data>
|
||||
<data name="AppElevatedDevNameAndVersion" xml:space="preserve">
|
||||
<value>Ху Тао Dev {0} [Администратор]</value>
|
||||
</data>
|
||||
<data name="AppElevatedNameAndVersion" xml:space="preserve">
|
||||
<value>Ху Тао {0} [Администратор]</value>
|
||||
</data>
|
||||
<data name="AppName" xml:space="preserve">
|
||||
<value>Snap Hutao</value>
|
||||
<value>Ху Тао</value>
|
||||
</data>
|
||||
<data name="AppNameAndVersion" xml:space="preserve">
|
||||
<value>Snap Hutao {0}</value>
|
||||
<value>Ху Тао {0}</value>
|
||||
</data>
|
||||
<data name="ContentDialogCancelCloseButtonText" xml:space="preserve">
|
||||
<value>Отмена</value>
|
||||
@@ -186,9 +192,6 @@
|
||||
<data name="FilePickerImportCommit" xml:space="preserve">
|
||||
<value>Импорт</value>
|
||||
</data>
|
||||
<data name="FilePickerPowerShellCommit" xml:space="preserve">
|
||||
<value>Выберите PowerShell</value>
|
||||
</data>
|
||||
<data name="GuideWindowTitle" xml:space="preserve">
|
||||
<value>Добро пожаловать в Snap Hutao, путешественник ~</value>
|
||||
</data>
|
||||
@@ -492,19 +495,19 @@
|
||||
<value>Волна 3: Волна появится только после убийства всех врагов в предыдущей волне.</value>
|
||||
</data>
|
||||
<data name="ModelMetadataTowerWaveTypeWave4" xml:space="preserve">
|
||||
<value>第四波:击败所有怪物,下一波才会出现</value>
|
||||
<value>Четвертая волна: Победите всех монстров, прежде чем появится следующая волна</value>
|
||||
</data>
|
||||
<data name="ModelNameValueDefaultDescription" xml:space="preserve">
|
||||
<value>请更新角色橱窗数据</value>
|
||||
<value>Обновите данные витрины персонажей</value>
|
||||
</data>
|
||||
<data name="ModelNameValueDefaultName" xml:space="preserve">
|
||||
<value>暂无数据</value>
|
||||
<value>Нет данных</value>
|
||||
</data>
|
||||
<data name="ModelWeaponAffixFormat" xml:space="preserve">
|
||||
<value>精炼 {0} 阶</value>
|
||||
<value>Уровень совершенствования {0}</value>
|
||||
</data>
|
||||
<data name="MustSelectUserAndUid" xml:space="preserve">
|
||||
<value>必须登录 米游社/HoYoLAB 并选择一个用户与角色</value>
|
||||
<value>Необходимо войти в учетную запись miHoYo/HoYoLAB и выбрать пользователя с персонажем</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceDeleteEntrySucceed" xml:space="preserve">
|
||||
<value>删除了 Uid:{0} 的 {1} 条祈愿记录</value>
|
||||
@@ -932,9 +935,6 @@
|
||||
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
|
||||
<value>未开启长路径功能,无法设置注册表键值</value>
|
||||
</data>
|
||||
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
|
||||
<value>找不到 PowerShell 的安装目录</value>
|
||||
</data>
|
||||
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
|
||||
<value>无法读取游戏配置文件 {0},可能是文件不存在</value>
|
||||
</data>
|
||||
@@ -1553,6 +1553,9 @@
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>切换账号失败</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
|
||||
<value>无法选择UID [{0}] 对应的账号 [{1}],该账号不属于当前服务器</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingActionComplete" xml:space="preserve">
|
||||
<value>操作完成</value>
|
||||
</data>
|
||||
@@ -2144,6 +2147,9 @@
|
||||
<data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve">
|
||||
<value>预下载</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSelectGamePath" xml:space="preserve">
|
||||
<value>选择游戏路径</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
|
||||
<value>该账号尚未绑定实时便笺通知 UID</value>
|
||||
</data>
|
||||
@@ -2276,6 +2282,15 @@
|
||||
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
|
||||
<value>设备 IP</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeDescription" xml:space="preserve">
|
||||
<value>管理员模式会影响部分功能的可用性与行为</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeHeader" xml:space="preserve">
|
||||
<value>管理员模式</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeRestartAction" xml:space="preserve">
|
||||
<value>以管理员身份重启</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
|
||||
<value>在祈愿记录页面显示或隐藏无记录的历史祈愿活动</value>
|
||||
</data>
|
||||
@@ -2432,12 +2447,6 @@
|
||||
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
|
||||
<value>设置游戏路径时,请选择游戏本体(YuanShen.exe 或 GenshinImpact.exe)而不是启动器(launcher.exe)</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
|
||||
<value>胡桃使用 PowerShell 更改注册表中的信息以修改游戏内账号</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
|
||||
<value>PowerShell 路径</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
|
||||
<value>Shell 体验</value>
|
||||
</data>
|
||||
@@ -2741,6 +2750,15 @@
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>已复制到剪贴板</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusFinished" xml:space="preserve">
|
||||
<value>全部完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusNotOpen" xml:space="preserve">
|
||||
<value>尚未开启</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusOngoing" xml:space="preserve">
|
||||
<value>进行中</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>已完成</value>
|
||||
</data>
|
||||
|
||||
@@ -120,6 +120,12 @@
|
||||
<data name="AppDevNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃 Dev {0}</value>
|
||||
</data>
|
||||
<data name="AppElevatedDevNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃Dev {0} [管理员]</value>
|
||||
</data>
|
||||
<data name="AppElevatedNameAndVersion" xml:space="preserve">
|
||||
<value>胡桃 {0} [管理员]</value>
|
||||
</data>
|
||||
<data name="AppName" xml:space="preserve">
|
||||
<value>胡桃</value>
|
||||
</data>
|
||||
@@ -142,7 +148,7 @@
|
||||
<value>無效的 Uri</value>
|
||||
</data>
|
||||
<data name="ControlImageCompositionImageHttpRequest" xml:space="preserve">
|
||||
<value>HTTP GET {0}</value>
|
||||
<value>获取HTTP{0}</value>
|
||||
</data>
|
||||
<data name="ControlImageCompositionImageSystemException" xml:space="preserve">
|
||||
<value>應用 CompositionImage 的源時發生異常</value>
|
||||
@@ -186,9 +192,6 @@
|
||||
<data name="FilePickerImportCommit" xml:space="preserve">
|
||||
<value>匯入</value>
|
||||
</data>
|
||||
<data name="FilePickerPowerShellCommit" xml:space="preserve">
|
||||
<value>選擇 PowerShell</value>
|
||||
</data>
|
||||
<data name="GuideWindowTitle" xml:space="preserve">
|
||||
<value>歡迎使用胡桃</value>
|
||||
</data>
|
||||
@@ -295,7 +298,7 @@
|
||||
<comment>Need EXACT same string in game</comment>
|
||||
</data>
|
||||
<data name="ModelIntrinsicBodyTypeBoy" xml:space="preserve">
|
||||
<value>少男</value>
|
||||
<value>少年</value>
|
||||
</data>
|
||||
<data name="ModelIntrinsicBodyTypeGirl" xml:space="preserve">
|
||||
<value>少女</value>
|
||||
@@ -307,7 +310,7 @@
|
||||
<value>蘿莉</value>
|
||||
</data>
|
||||
<data name="ModelIntrinsicBodyTypeMale" xml:space="preserve">
|
||||
<value>成男</value>
|
||||
<value>成年男子</value>
|
||||
</data>
|
||||
<data name="ModelIntrinsicElementNameElec" xml:space="preserve">
|
||||
<value>雷</value>
|
||||
@@ -376,7 +379,7 @@
|
||||
<comment>Need EXACT same string in game</comment>
|
||||
</data>
|
||||
<data name="ModelMetadataAvatarPlayerName" xml:space="preserve">
|
||||
<value>旅行者</value>
|
||||
<value>旅人</value>
|
||||
</data>
|
||||
<data name="ModelMetadataFetterInfoBirthdayFormat" xml:space="preserve">
|
||||
<value>{0} 月 {1} 日</value>
|
||||
@@ -483,7 +486,7 @@
|
||||
<value>第一波:擊敗所有怪物,下一波才會出現</value>
|
||||
</data>
|
||||
<data name="ModelMetadataTowerWaveTypeWave1Additional" xml:space="preserve">
|
||||
<value>第一波附加:增援第一波怪物</value>
|
||||
<value>第一波追加:增援第一波怪物</value>
|
||||
</data>
|
||||
<data name="ModelMetadataTowerWaveTypeWave2" xml:space="preserve">
|
||||
<value>第二波:擊敗所有怪物,下一波才會出現</value>
|
||||
@@ -843,7 +846,7 @@
|
||||
<value>找不到原神內置瀏覽器緩存路徑:\n{0}</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogUrlProviderCacheUrlNotFound" xml:space="preserve">
|
||||
<value>未找到可用的 Url</value>
|
||||
<value>找不到可用的 Url</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogUrlProviderManualInputInvalid" xml:space="preserve">
|
||||
<value>提供的 Url 無效</value>
|
||||
@@ -932,9 +935,6 @@
|
||||
<data name="ServiceGameRegisteryInteropLongPathsDisabled" xml:space="preserve">
|
||||
<value>未開啓長路徑功能,無法設定注冊表鍵值</value>
|
||||
</data>
|
||||
<data name="ServiceGameRegisteryInteropPowershellNotFound" xml:space="preserve">
|
||||
<value>找不到 PowerShell 的安裝目錄</value>
|
||||
</data>
|
||||
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
|
||||
<value>無法讀取遊戲設定檔 {0},可能是檔案不存在</value>
|
||||
</data>
|
||||
@@ -996,7 +996,7 @@
|
||||
<value>不支援的 UIGF 版本</value>
|
||||
</data>
|
||||
<data name="ServiceUpdateStatusVersionDescription" xml:space="preserve">
|
||||
<value>发现新版本 {0}</value>
|
||||
<value>發現新版本 {0}</value>
|
||||
</data>
|
||||
<data name="ServiceUserCurrentMultiMatched" xml:space="preserve">
|
||||
<value>已选中多条用户记录</value>
|
||||
@@ -1553,6 +1553,9 @@
|
||||
<data name="ViewModelLaunchGameSwitchGameAccountFail" xml:space="preserve">
|
||||
<value>切換帳號失敗</value>
|
||||
</data>
|
||||
<data name="ViewModelLaunchGameUnableToSwitchUidAttachedGameAccount" xml:space="preserve">
|
||||
<value>无法选择UID [{0}] 对应的账号 [{1}],该账号不属于当前服务器</value>
|
||||
</data>
|
||||
<data name="ViewModelSettingActionComplete" xml:space="preserve">
|
||||
<value>操作完成</value>
|
||||
</data>
|
||||
@@ -2144,6 +2147,9 @@
|
||||
<data name="ViewPageLaunchGameResourcePreDownloadHeader" xml:space="preserve">
|
||||
<value>預下載</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSelectGamePath" xml:space="preserve">
|
||||
<value>选择游戏路径</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameSwitchAccountAttachUidNull" xml:space="preserve">
|
||||
<value>該用戶尚未綁定即時便箋通知 UID</value>
|
||||
</data>
|
||||
@@ -2276,6 +2282,15 @@
|
||||
<data name="ViewPageSettingDeviceIpHeader" xml:space="preserve">
|
||||
<value>設備 IP</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeDescription" xml:space="preserve">
|
||||
<value>管理员模式会影响部分功能的可用性与行为</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeHeader" xml:space="preserve">
|
||||
<value>管理员模式</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingElevatedModeRestartAction" xml:space="preserve">
|
||||
<value>以管理员身份重启</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingEmptyHistoryVisibleDescription" xml:space="preserve">
|
||||
<value>在祈願紀錄頁面顯示或隱藏無記錄的歷史祈願活動</value>
|
||||
</data>
|
||||
@@ -2432,12 +2447,6 @@
|
||||
<data name="ViewPageSettingSetGamePathHint" xml:space="preserve">
|
||||
<value>設置游戲路徑時,請選擇游戲本體(YuanShen.exe 或 GenshinImpact.exe) 而不是啓動器(launcher.exe)</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellDescription" xml:space="preserve">
|
||||
<value>胡桃使用 PowerShell 更改註冊表中的信息以修改遊戲內賬號</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingSetPowerShellPathHeader" xml:space="preserve">
|
||||
<value>PowerShell 路徑</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingShellExperienceHeader" xml:space="preserve">
|
||||
<value>Shell 體驗</value>
|
||||
</data>
|
||||
@@ -2741,6 +2750,15 @@
|
||||
<data name="WebBridgeShareCopyToClipboardSuccess" xml:space="preserve">
|
||||
<value>已複製到剪貼簿</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusFinished" xml:space="preserve">
|
||||
<value>全部完成</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusNotOpen" xml:space="preserve">
|
||||
<value>尚未开启</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteArchonQuestStatusOngoing" xml:space="preserve">
|
||||
<value>进行中</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteAttendanceRewardStatusFinishedNonReward" xml:space="preserve">
|
||||
<value>已完成</value>
|
||||
</data>
|
||||
|
||||
@@ -7,7 +7,6 @@ using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Extensions.Primitives;
|
||||
using Snap.Hutao.Core.Windowing;
|
||||
using Snap.Hutao.Model;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service;
|
||||
|
||||
@@ -16,42 +14,12 @@ namespace Snap.Hutao.Service;
|
||||
[Injection(InjectAs.Singleton)]
|
||||
internal sealed partial class AppOptions : DbStoreOptions
|
||||
{
|
||||
private string? powerShellPath;
|
||||
private bool? isEmptyHistoryWishVisible;
|
||||
private BackdropType? backdropType;
|
||||
private CultureInfo? currentCulture;
|
||||
private Region? region;
|
||||
private string? geetestCustomCompositeUrl;
|
||||
|
||||
public string PowerShellPath
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetOption(ref powerShellPath, SettingEntry.PowerShellPath, GetDefaultPowerShellLocationOrEmpty);
|
||||
|
||||
static string GetDefaultPowerShellLocationOrEmpty()
|
||||
{
|
||||
string? paths = Environment.GetEnvironmentVariable("Path");
|
||||
if (!string.IsNullOrEmpty(paths))
|
||||
{
|
||||
foreach (StringSegment path in new StringTokenizer(paths, [';']))
|
||||
{
|
||||
if (path is { HasValue: true, Length: > 0 })
|
||||
{
|
||||
if (path.Value.Contains("WindowsPowerShell", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Path.Combine(path.Value, "powershell.exe");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
set => SetOption(ref powerShellPath, SettingEntry.PowerShellPath, value);
|
||||
}
|
||||
|
||||
public bool IsEmptyHistoryWishVisible
|
||||
{
|
||||
get => GetOption(ref isEmptyHistoryWishVisible, SettingEntry.IsEmptyHistoryWishVisible);
|
||||
|
||||
@@ -58,7 +58,11 @@ internal sealed partial class DailyNoteOptions : DbStoreOptions
|
||||
{
|
||||
if (SelectedRefreshTime is not null)
|
||||
{
|
||||
scheduleTaskInterop.RegisterForDailyNoteRefresh(SelectedRefreshTime.Value);
|
||||
if (!scheduleTaskInterop.RegisterForDailyNoteRefresh(SelectedRefreshTime.Value))
|
||||
{
|
||||
serviceProvider.GetRequiredService<IInfoBarService>().Warning(SH.ViewModelDailyNoteRegisterTaskFail);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -77,7 +77,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
|
||||
|
||||
List<DailyNoteEntry> entryList = await dailyNoteDbService.GetDailyNoteEntryIncludeUserListAsync().ConfigureAwait(false);
|
||||
entryList.ForEach(entry => { entry.UserGameRole = userService.GetUserGameRoleByUid(entry.Uid); });
|
||||
entries = new(entryList);
|
||||
entries = entryList.ToObservableCollection();
|
||||
}
|
||||
|
||||
return entries;
|
||||
@@ -147,7 +147,7 @@ internal sealed partial class DailyNoteService : IDailyNoteService, IRecipient<U
|
||||
// 发送通知必须早于数据库更新,否则会导致通知重复
|
||||
await dailyNoteNotificationOperation.SendAsync(entry).ConfigureAwait(false);
|
||||
await dailyNoteDbService.UpdateDailyNoteEntryAsync(entry).ConfigureAwait(false);
|
||||
await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(dailyNote).ConfigureAwait(false);
|
||||
await dailyNoteWebhookOperation.TryPostDailyNoteToWebhookAsync(entry.Uid, dailyNote).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Request.Builder;
|
||||
using Snap.Hutao.Web.Request.Builder.Abstraction;
|
||||
using System.Net.Http;
|
||||
@@ -18,7 +19,7 @@ internal sealed partial class DailyNoteWebhookOperation
|
||||
private readonly DailyNoteOptions dailyNoteOptions;
|
||||
private readonly HttpClient httpClient;
|
||||
|
||||
public async ValueTask TryPostDailyNoteToWebhookAsync(WebDailyNote dailyNote, CancellationToken token = default)
|
||||
public async ValueTask TryPostDailyNoteToWebhookAsync(PlayerUid playerUid, WebDailyNote dailyNote, CancellationToken token = default)
|
||||
{
|
||||
string? targetUrl = dailyNoteOptions.WebhookUrl;
|
||||
if (string.IsNullOrEmpty(targetUrl) || !Uri.TryCreate(targetUrl, UriKind.Absolute, out Uri? targetUri))
|
||||
@@ -28,6 +29,7 @@ internal sealed partial class DailyNoteWebhookOperation
|
||||
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(targetUri)
|
||||
.SetHeader("x-uid", $"{playerUid}")
|
||||
.PostJson(dailyNote);
|
||||
|
||||
await builder.TryCatchSendAsync(httpClient, logger, token).ConfigureAwait(false);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Discord.GameSDK;
|
||||
using Snap.Discord.GameSDK.ABI;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Unicode;
|
||||
|
||||
namespace Snap.Hutao.Service.Discord;
|
||||
|
||||
@@ -18,75 +18,94 @@ internal static class DiscordController
|
||||
private static readonly CancellationTokenSource StopTokenSource = new();
|
||||
private static readonly object SyncRoot = new();
|
||||
|
||||
private static Snap.Discord.GameSDK.Discord? discordManager;
|
||||
private static long currentClientId;
|
||||
private static unsafe IDiscordCore* discordCorePtr;
|
||||
private static bool isInitialized;
|
||||
|
||||
public static async ValueTask<Result> SetDefaultActivityAsync(DateTimeOffset startTime)
|
||||
public static async ValueTask<DiscordResult> SetDefaultActivityAsync(DateTimeOffset startTime)
|
||||
{
|
||||
ResetManagerOrIgnore(HutaoAppId);
|
||||
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
||||
return SetDefaultActivity(startTime);
|
||||
|
||||
if (discordManager is null)
|
||||
static unsafe DiscordResult SetDefaultActivity(in DateTimeOffset startTime)
|
||||
{
|
||||
return Result.Ok;
|
||||
ResetManagerOrIgnore(HutaoAppId);
|
||||
|
||||
if (discordCorePtr is null)
|
||||
{
|
||||
return DiscordResult.Ok;
|
||||
}
|
||||
|
||||
IDiscordActivityManager* activityManagerPtr = discordCorePtr->get_activity_manager(discordCorePtr);
|
||||
|
||||
DiscordActivity activity = default;
|
||||
activity.timestamps.start = startTime.ToUnixTimeSeconds();
|
||||
SetString(activity.assets.large_image, 128, "icon"u8);
|
||||
SetString(activity.assets.large_text, 128, SH.AppName);
|
||||
|
||||
return new DiscordUpdateActivityAsyncAction(activityManagerPtr).WaitUpdateActivity(activity);
|
||||
}
|
||||
|
||||
ActivityManager activityManager = discordManager.GetActivityManager();
|
||||
|
||||
Activity activity = default;
|
||||
activity.Timestamps.Start = startTime.ToUnixTimeSeconds();
|
||||
activity.Assets.LargeImage = "icon";
|
||||
activity.Assets.LargeText = SH.AppName;
|
||||
|
||||
return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async ValueTask<Result> SetPlayingYuanShenAsync()
|
||||
public static async ValueTask<DiscordResult> SetPlayingYuanShenAsync()
|
||||
{
|
||||
ResetManagerOrIgnore(YuanshenId);
|
||||
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
||||
return SetPlayingYuanShen();
|
||||
|
||||
if (discordManager is null)
|
||||
static unsafe DiscordResult SetPlayingYuanShen()
|
||||
{
|
||||
return Result.Ok;
|
||||
ResetManagerOrIgnore(YuanshenId);
|
||||
|
||||
if (discordCorePtr is null)
|
||||
{
|
||||
return DiscordResult.Ok;
|
||||
}
|
||||
|
||||
IDiscordActivityManager* activityManagerPtr = discordCorePtr->get_activity_manager(discordCorePtr);
|
||||
|
||||
DiscordActivity activity = default;
|
||||
SetString(activity.state, 128, SH.FormatServiceDiscordGameLaunchedBy(SH.AppName));
|
||||
SetString(activity.details, 128, SH.ServiceDiscordGameActivityDetails);
|
||||
activity.timestamps.start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
SetString(activity.assets.large_image, 128, "icon"u8);
|
||||
SetString(activity.assets.large_text, 128, "原神"u8);
|
||||
SetString(activity.assets.small_image, 128, "app"u8);
|
||||
SetString(activity.assets.small_text, 128, SH.AppName);
|
||||
|
||||
return new DiscordUpdateActivityAsyncAction(activityManagerPtr).WaitUpdateActivity(activity);
|
||||
}
|
||||
|
||||
ActivityManager activityManager = discordManager.GetActivityManager();
|
||||
|
||||
Activity activity = default;
|
||||
activity.State = SH.FormatServiceDiscordGameLaunchedBy(SH.AppName);
|
||||
activity.Details = SH.ServiceDiscordGameActivityDetails;
|
||||
activity.Timestamps.Start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
activity.Assets.LargeImage = "icon";
|
||||
activity.Assets.LargeText = "原神";
|
||||
activity.Assets.SmallImage = "app";
|
||||
activity.Assets.SmallText = SH.AppName;
|
||||
|
||||
return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static async ValueTask<Result> SetPlayingGenshinImpactAsync()
|
||||
public static async ValueTask<DiscordResult> SetPlayingGenshinImpactAsync()
|
||||
{
|
||||
ResetManagerOrIgnore(GenshinImpactId);
|
||||
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
||||
return SetPlayingGenshinImpact();
|
||||
|
||||
if (discordManager is null)
|
||||
static unsafe DiscordResult SetPlayingGenshinImpact()
|
||||
{
|
||||
return Result.Ok;
|
||||
ResetManagerOrIgnore(GenshinImpactId);
|
||||
|
||||
if (discordCorePtr is null)
|
||||
{
|
||||
return DiscordResult.Ok;
|
||||
}
|
||||
|
||||
IDiscordActivityManager* activityManagerPtr = discordCorePtr->get_activity_manager(discordCorePtr);
|
||||
|
||||
DiscordActivity activity = default;
|
||||
SetString(activity.state, 128, SH.FormatServiceDiscordGameLaunchedBy(SH.AppName));
|
||||
SetString(activity.details, 128, SH.ServiceDiscordGameActivityDetails);
|
||||
activity.timestamps.start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
SetString(activity.assets.large_image, 128, "icon"u8);
|
||||
SetString(activity.assets.large_text, 128, "Genshin Impact"u8);
|
||||
SetString(activity.assets.small_image, 128, "app"u8);
|
||||
SetString(activity.assets.small_text, 128, SH.AppName);
|
||||
|
||||
return new DiscordUpdateActivityAsyncAction(activityManagerPtr).WaitUpdateActivity(activity);
|
||||
}
|
||||
|
||||
ActivityManager activityManager = discordManager.GetActivityManager();
|
||||
|
||||
Activity activity = default;
|
||||
activity.State = SH.FormatServiceDiscordGameLaunchedBy(SH.AppName);
|
||||
activity.Details = SH.ServiceDiscordGameActivityDetails;
|
||||
activity.Timestamps.Start = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
activity.Assets.LargeImage = "icon";
|
||||
activity.Assets.LargeText = "Genshin Impact";
|
||||
activity.Assets.SmallImage = "app";
|
||||
activity.Assets.SmallText = SH.AppName;
|
||||
|
||||
return await activityManager.UpdateActivityAsync(activity).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public static void Stop()
|
||||
public static unsafe void Stop()
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
@@ -98,7 +117,7 @@ internal static class DiscordController
|
||||
StopTokenSource.Cancel();
|
||||
try
|
||||
{
|
||||
discordManager?.Dispose();
|
||||
discordCorePtr = default;
|
||||
}
|
||||
catch (SEHException)
|
||||
{
|
||||
@@ -108,23 +127,30 @@ internal static class DiscordController
|
||||
|
||||
private static unsafe void ResetManagerOrIgnore(long clientId)
|
||||
{
|
||||
if (discordManager?.ClientId == clientId)
|
||||
if (currentClientId == clientId)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Actually requires a discord client to be running on Windows platform.
|
||||
// If not, the following creation code will throw.
|
||||
if (System.Diagnostics.Process.GetProcessesByName("Discord").Length == 0)
|
||||
if (System.Diagnostics.Process.GetProcessesByName("Discord").Length <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (SyncRoot)
|
||||
{
|
||||
discordManager?.Dispose();
|
||||
discordManager = new(clientId, CreateFlags.NoRequireDiscord);
|
||||
discordManager.SetLogHook(Snap.Discord.GameSDK.LogLevel.Debug, SetLogHookHandler.Create(&DebugWriteDiscordMessage));
|
||||
DiscordCreateParams @params = default;
|
||||
Methods.DiscordCreateParamsSetDefault(&@params);
|
||||
@params.client_id = clientId;
|
||||
@params.flags = (uint)DiscordCreateFlags.Default;
|
||||
IDiscordCore* ptr = default;
|
||||
Methods.DiscordCreate(3, &@params, &ptr);
|
||||
|
||||
currentClientId = clientId;
|
||||
discordCorePtr = ptr;
|
||||
discordCorePtr->set_log_hook(discordCorePtr, DiscordLogLevel.Debug, default, &DebugWriteDiscordMessage);
|
||||
}
|
||||
|
||||
if (isInitialized)
|
||||
@@ -135,10 +161,10 @@ internal static class DiscordController
|
||||
DiscordRunCallbacksAsync(StopTokenSource.Token).SafeForget();
|
||||
isInitialized = true;
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
|
||||
static unsafe void DebugWriteDiscordMessage(Snap.Discord.GameSDK.LogLevel logLevel, byte* ptr)
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
static unsafe void DebugWriteDiscordMessage(void* state, DiscordLogLevel logLevel, sbyte* ptr)
|
||||
{
|
||||
ReadOnlySpan<byte> utf8 = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(ptr);
|
||||
ReadOnlySpan<byte> utf8 = MemoryMarshal.CreateReadOnlySpanFromNullTerminated((byte*)ptr);
|
||||
string message = System.Text.Encoding.UTF8.GetString(utf8);
|
||||
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK]:[{logLevel}]:{message}");
|
||||
}
|
||||
@@ -146,7 +172,7 @@ internal static class DiscordController
|
||||
|
||||
private static async ValueTask DiscordRunCallbacksAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
using (PeriodicTimer timer = new(TimeSpan.FromMilliseconds(1000)))
|
||||
using (PeriodicTimer timer = new(TimeSpan.FromMilliseconds(500)))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -161,15 +187,10 @@ internal static class DiscordController
|
||||
{
|
||||
try
|
||||
{
|
||||
discordManager?.RunCallbacks();
|
||||
}
|
||||
catch (ResultException ex)
|
||||
{
|
||||
// If result is Ok
|
||||
// Maybe the connection is reset.
|
||||
if (ex.Result is not Result.Ok)
|
||||
DiscordResult result = DiscordCoreRunRunCallbacks();
|
||||
if (result is not DiscordResult.Ok)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{ex.Result:D} {ex.Result}");
|
||||
System.Diagnostics.Debug.WriteLine($"[Discord.GameSDK ERROR]:{result:D} {result}");
|
||||
}
|
||||
}
|
||||
catch (SEHException ex)
|
||||
@@ -185,5 +206,70 @@ internal static class DiscordController
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
unsafe DiscordResult DiscordCoreRunRunCallbacks()
|
||||
{
|
||||
if (discordCorePtr is not null)
|
||||
{
|
||||
return discordCorePtr->run_callbacks(discordCorePtr);
|
||||
}
|
||||
|
||||
return DiscordResult.Ok;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void SetString(sbyte* reference, int length, string source)
|
||||
{
|
||||
Span<sbyte> sbytes = new(reference, length);
|
||||
sbytes.Clear();
|
||||
Utf8.FromUtf16(source.AsSpan(), MemoryMarshal.Cast<sbyte, byte>(sbytes), out _, out _);
|
||||
}
|
||||
|
||||
private static unsafe void SetString(sbyte* reference, int length, in ReadOnlySpan<byte> source)
|
||||
{
|
||||
Span<sbyte> sbytes = new(reference, length);
|
||||
sbytes.Clear();
|
||||
source.CopyTo(MemoryMarshal.Cast<sbyte, byte>(sbytes));
|
||||
}
|
||||
|
||||
private struct DiscordAsyncAction
|
||||
{
|
||||
public DiscordResult Result;
|
||||
public bool IsCompleted;
|
||||
}
|
||||
|
||||
private unsafe struct DiscordUpdateActivityAsyncAction
|
||||
{
|
||||
private readonly IDiscordActivityManager* activityManagerPtr;
|
||||
private DiscordAsyncAction discordAsyncAction;
|
||||
|
||||
public DiscordUpdateActivityAsyncAction(IDiscordActivityManager* activityManagerPtr)
|
||||
{
|
||||
this.activityManagerPtr = activityManagerPtr;
|
||||
}
|
||||
|
||||
public DiscordResult WaitUpdateActivity(DiscordActivity activity)
|
||||
{
|
||||
fixed (DiscordAsyncAction* actionPtr = &discordAsyncAction)
|
||||
{
|
||||
activityManagerPtr->update_activity(activityManagerPtr, &activity, actionPtr, &HandleResult);
|
||||
}
|
||||
|
||||
SpinWaitPolyfill.SpinUntil(ref discordAsyncAction, &CheckActionCompleted);
|
||||
return discordAsyncAction.Result;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
|
||||
private static void HandleResult(void* state, DiscordResult result)
|
||||
{
|
||||
DiscordAsyncAction* action = (DiscordAsyncAction*)state;
|
||||
action->Result = result;
|
||||
action->IsCompleted = true;
|
||||
}
|
||||
|
||||
private static bool CheckActionCompleted(ref readonly DiscordAsyncAction state)
|
||||
{
|
||||
return state.IsCompleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,14 +11,14 @@ internal sealed partial class DiscordService : IDiscordService, IDisposable
|
||||
{
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
|
||||
public async ValueTask SetPlayingActivity(bool isOversea)
|
||||
public async ValueTask SetPlayingActivityAsync(bool isOversea)
|
||||
{
|
||||
_ = isOversea
|
||||
? await DiscordController.SetPlayingGenshinImpactAsync().ConfigureAwait(false)
|
||||
: await DiscordController.SetPlayingYuanShenAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public async ValueTask SetNormalActivity()
|
||||
public async ValueTask SetNormalActivityAsync()
|
||||
{
|
||||
_ = await DiscordController.SetDefaultActivityAsync(runtimeOptions.AppLaunchTime).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Snap.Hutao.Service.Discord;
|
||||
|
||||
internal interface IDiscordService
|
||||
{
|
||||
ValueTask SetNormalActivity();
|
||||
ValueTask SetNormalActivityAsync();
|
||||
|
||||
ValueTask SetPlayingActivity(bool isOversea);
|
||||
ValueTask SetPlayingActivityAsync(bool isOversea);
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
@@ -16,7 +17,6 @@ internal sealed partial class GameAccountService : IGameAccountService
|
||||
private readonly IContentDialogFactory contentDialogFactory;
|
||||
private readonly IGameDbService gameDbService;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly AppOptions appOptions;
|
||||
|
||||
private ObservableCollection<GameAccount>? gameAccounts;
|
||||
|
||||
@@ -25,77 +25,66 @@ internal sealed partial class GameAccountService : IGameAccountService
|
||||
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
|
||||
}
|
||||
|
||||
public async ValueTask<GameAccount?> DetectGameAccountAsync()
|
||||
public async ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gameAccounts);
|
||||
|
||||
string? registrySdk = RegistryInterop.Get();
|
||||
if (!string.IsNullOrEmpty(registrySdk))
|
||||
if (schemeType is SchemeType.ChineseBilibili)
|
||||
{
|
||||
GameAccount? account = null;
|
||||
try
|
||||
{
|
||||
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
|
||||
}
|
||||
|
||||
if (account is null)
|
||||
{
|
||||
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
|
||||
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
account = GameAccount.From(name, registrySdk);
|
||||
|
||||
// sync database
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
|
||||
|
||||
// sync cache
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
gameAccounts.Add(account);
|
||||
}
|
||||
}
|
||||
|
||||
return account;
|
||||
return default;
|
||||
}
|
||||
|
||||
return default;
|
||||
string? registrySdk = RegistryInterop.Get(schemeType);
|
||||
if (string.IsNullOrEmpty(registrySdk))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
GameAccount? account = SingleGameAccountOrDefault(gameAccounts, registrySdk);
|
||||
if (account is null)
|
||||
{
|
||||
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
|
||||
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
account = GameAccount.From(name, registrySdk, schemeType);
|
||||
|
||||
// sync database
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
await gameDbService.AddGameAccountAsync(account).ConfigureAwait(false);
|
||||
|
||||
// sync cache
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
gameAccounts.Add(account);
|
||||
}
|
||||
}
|
||||
|
||||
return account;
|
||||
}
|
||||
|
||||
public GameAccount? DetectCurrentGameAccount()
|
||||
public GameAccount? DetectCurrentGameAccount(SchemeType schemeType)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gameAccounts);
|
||||
|
||||
string? registrySdk = RegistryInterop.Get();
|
||||
|
||||
if (!string.IsNullOrEmpty(registrySdk))
|
||||
if (schemeType is SchemeType.ChineseBilibili)
|
||||
{
|
||||
try
|
||||
{
|
||||
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
|
||||
}
|
||||
return default;
|
||||
}
|
||||
|
||||
return null;
|
||||
string? registrySdk = RegistryInterop.Get(schemeType);
|
||||
|
||||
if (string.IsNullOrEmpty(registrySdk))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return SingleGameAccountOrDefault(gameAccounts, registrySdk);
|
||||
}
|
||||
|
||||
public bool SetGameAccount(GameAccount account)
|
||||
{
|
||||
if (string.IsNullOrEmpty(appOptions.PowerShellPath))
|
||||
{
|
||||
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropPowershellNotFound, default!);
|
||||
}
|
||||
|
||||
return RegistryInterop.Set(account, appOptions.PowerShellPath);
|
||||
return RegistryInterop.Set(account);
|
||||
}
|
||||
|
||||
public void AttachGameAccountToUid(GameAccount gameAccount, string uid)
|
||||
@@ -106,12 +95,12 @@ internal sealed partial class GameAccountService : IGameAccountService
|
||||
|
||||
public async ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
LaunchGameAccountNameDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGameAccountNameDialog>().ConfigureAwait(false);
|
||||
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(true);
|
||||
(bool isOk, string name) = await dialog.GetInputNameAsync().ConfigureAwait(false);
|
||||
|
||||
if (isOk)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
gameAccount.UpdateName(name);
|
||||
|
||||
// sync database
|
||||
@@ -122,11 +111,24 @@ internal sealed partial class GameAccountService : IGameAccountService
|
||||
|
||||
public async ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ArgumentNullException.ThrowIfNull(gameAccounts);
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
gameAccounts.Remove(gameAccount);
|
||||
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
await gameDbService.RemoveGameAccountByIdAsync(gameAccount.InnerId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static GameAccount? SingleGameAccountOrDefault(ObservableCollection<GameAccount> gameAccounts, string registrySdk)
|
||||
{
|
||||
try
|
||||
{
|
||||
return gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
throw ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Account;
|
||||
@@ -12,9 +13,9 @@ internal interface IGameAccountService
|
||||
|
||||
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
|
||||
|
||||
GameAccount? DetectCurrentGameAccount();
|
||||
GameAccount? DetectCurrentGameAccount(SchemeType schemeType);
|
||||
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync();
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType);
|
||||
|
||||
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
using Microsoft.Win32;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
@@ -16,52 +15,23 @@ namespace Snap.Hutao.Service.Game.Account;
|
||||
/// </summary>
|
||||
internal static class RegistryInterop
|
||||
{
|
||||
private const string GenshinPath = @"Software\miHoYo\原神";
|
||||
private const string GenshinKey = $@"HKEY_CURRENT_USER\{GenshinPath}";
|
||||
private const string SdkChineseKey = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
|
||||
private const string ChineseKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\原神";
|
||||
private const string OverseaKeyName = @"HKEY_CURRENT_USER\Software\miHoYo\Genshin Impact";
|
||||
private const string SdkChineseValueName = "MIHOYOSDK_ADL_PROD_CN_h3123967166";
|
||||
private const string SdkOverseaValueName = "MIHOYOSDK_ADL_PROD_OVERSEA_h1158948810";
|
||||
|
||||
/// <summary>
|
||||
/// 设置键值
|
||||
/// 需要支持
|
||||
/// https://learn.microsoft.com/zh-cn/windows/win32/fileio/maximum-file-path-limitation
|
||||
/// </summary>
|
||||
/// <param name="account">账户</param>
|
||||
/// <param name="powerShellPath">PowerShell 路径</param>
|
||||
/// <returns>账号是否设置</returns>
|
||||
public static bool Set(GameAccount? account, string powerShellPath)
|
||||
private const string WindowsHDROnValueName = "WINDOWS_HDR_ON_h3132281285";
|
||||
|
||||
public static bool Set(GameAccount? account)
|
||||
{
|
||||
if (account is not null)
|
||||
{
|
||||
// 存回注册表的字节需要 '\0' 结尾
|
||||
Encoding.UTF8.GetByteCount(account.MihoyoSDK);
|
||||
byte[] tempBytes = Encoding.UTF8.GetBytes(account.MihoyoSDK);
|
||||
byte[] target = new byte[tempBytes.Length + 1];
|
||||
tempBytes.CopyTo(target, 0);
|
||||
byte[] target = [.. Encoding.UTF8.GetBytes(account.MihoyoSDK), 0];
|
||||
(string keyName, string valueName) = GetKeyValueName(account.Type);
|
||||
Registry.SetValue(keyName, valueName, target);
|
||||
|
||||
string base64 = Convert.ToBase64String(target);
|
||||
string path = $"HKCU:{GenshinPath}";
|
||||
string command = $"""
|
||||
-Command "$value = [Convert]::FromBase64String('{base64}'); Set-ItemProperty -Path '{path}' -Name '{SdkChineseKey}' -Value $value -Force;"
|
||||
""";
|
||||
|
||||
ProcessStartInfo startInfo = new()
|
||||
{
|
||||
Arguments = command,
|
||||
WorkingDirectory = Path.GetDirectoryName(powerShellPath),
|
||||
CreateNoWindow = true,
|
||||
FileName = powerShellPath,
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(startInfo)?.WaitForExit();
|
||||
}
|
||||
catch (Win32Exception ex)
|
||||
{
|
||||
ThrowHelper.RuntimeEnvironment(SH.ServiceGameRegisteryInteropLongPathsDisabled, ex);
|
||||
}
|
||||
|
||||
if (Get() == account.MihoyoSDK)
|
||||
if (Get(account.Type) == account.MihoyoSDK)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -70,24 +40,37 @@ internal static class RegistryInterop
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 在注册表中获取账号信息
|
||||
/// </summary>
|
||||
/// <returns>当前注册表中的信息</returns>
|
||||
public static unsafe string? Get()
|
||||
public static unsafe string? Get(SchemeType scheme)
|
||||
{
|
||||
object? sdk = Registry.GetValue(GenshinKey, SdkChineseKey, Array.Empty<byte>());
|
||||
(string keyName, string valueName) = GetKeyValueName(scheme);
|
||||
object? sdk = Registry.GetValue(keyName, valueName, Array.Empty<byte>());
|
||||
|
||||
if (sdk is byte[] bytes)
|
||||
if (sdk is not byte[] bytes)
|
||||
{
|
||||
fixed (byte* pByte = bytes)
|
||||
{
|
||||
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
|
||||
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
|
||||
return Encoding.UTF8.GetString(span);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
fixed (byte* pByte = bytes)
|
||||
{
|
||||
// 从注册表获取的字节数组带有 '\0' 结尾,需要舍去
|
||||
ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pByte);
|
||||
return Encoding.UTF8.GetString(span);
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetWindowsHDR(bool isOversea)
|
||||
{
|
||||
string keyName = isOversea ? OverseaKeyName : ChineseKeyName;
|
||||
Registry.SetValue(keyName, WindowsHDROnValueName, 1);
|
||||
}
|
||||
|
||||
private static (string KeyName, string ValueName) GetKeyValueName(SchemeType scheme)
|
||||
{
|
||||
return scheme switch
|
||||
{
|
||||
SchemeType.ChineseOfficial => (ChineseKeyName, SdkChineseValueName),
|
||||
SchemeType.Oversea => (OverseaKeyName, SdkOverseaValueName),
|
||||
_ => throw ThrowHelper.NotSupported($"Invalid account SchemeType: {scheme}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -34,21 +34,6 @@ internal readonly struct ChannelOptions
|
||||
/// </summary>
|
||||
public readonly string? ConfigFilePath;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的多通道
|
||||
/// </summary>
|
||||
/// <param name="channel">通道</param>
|
||||
/// <param name="subChannel">子通道</param>
|
||||
/// <param name="isOversea">是否为国际服</param>
|
||||
/// <param name="configFilePath">配置文件路径</param>
|
||||
public ChannelOptions(string? channel, string? subChannel, bool isOversea, string? configFilePath = null)
|
||||
{
|
||||
_ = Enum.TryParse(channel, out Channel);
|
||||
_ = Enum.TryParse(subChannel, out SubChannel);
|
||||
IsOversea = isOversea;
|
||||
ConfigFilePath = configFilePath;
|
||||
}
|
||||
|
||||
public ChannelOptions(ChannelType channel, SubChannelType subChannel, bool isOversea)
|
||||
{
|
||||
Channel = channel;
|
||||
@@ -56,24 +41,33 @@ internal readonly struct ChannelOptions
|
||||
IsOversea = isOversea;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 配置文件未找到
|
||||
/// </summary>
|
||||
/// <param name="isOversea">是否为国际服</param>
|
||||
/// <param name="configFilePath">配置文件期望路径</param>
|
||||
/// <returns>选项</returns>
|
||||
public ChannelOptions(string? channel, string? subChannel, bool isOversea)
|
||||
{
|
||||
_ = Enum.TryParse(channel, out Channel);
|
||||
_ = Enum.TryParse(subChannel, out SubChannel);
|
||||
IsOversea = isOversea;
|
||||
}
|
||||
|
||||
private ChannelOptions(bool isOversea, string? configFilePath)
|
||||
{
|
||||
IsOversea = isOversea;
|
||||
ConfigFilePath = configFilePath;
|
||||
}
|
||||
|
||||
public static ChannelOptions FileNotFound(bool isOversea, string configFilePath)
|
||||
{
|
||||
return new(null, null, isOversea, configFilePath);
|
||||
return new(isOversea, configFilePath);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[ChannelType:{Channel}] [SubChannel:{SubChannel}] [IsOversea: {IsOversea}]";
|
||||
return $$"""
|
||||
{ ChannelType: {{Channel}}, SubChannel: {{SubChannel}}, IsOversea: {{IsOversea}}}
|
||||
""";
|
||||
}
|
||||
|
||||
// DO NOT DELETE used in HashSet
|
||||
// DO NOT DELETE, used in HashSet
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return HashCode.Combine(Channel, SubChannel, IsOversea);
|
||||
|
||||
@@ -17,9 +17,12 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
|
||||
|
||||
public ChannelOptions GetChannelOptions()
|
||||
{
|
||||
string gamePath = launchOptions.GamePath;
|
||||
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFileName);
|
||||
bool isOversea = string.Equals(Path.GetFileName(gamePath), GenshinImpactFileName, StringComparison.OrdinalIgnoreCase);
|
||||
if (!launchOptions.TryGetGamePathAndFilePathByName(ConfigFileName, out string gamePath, out string? configPath))
|
||||
{
|
||||
throw ThrowHelper.InvalidOperation($"Invalid game path: {gamePath}");
|
||||
}
|
||||
|
||||
bool isOversea = LaunchScheme.ExecutableIsOversea(Path.GetFileName(gamePath));
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
@@ -35,61 +38,4 @@ internal sealed partial class GameChannelOptionsService : IGameChannelOptionsSer
|
||||
return new(channel, subChannel, isOversea);
|
||||
}
|
||||
}
|
||||
|
||||
public bool SetChannelOptions(LaunchScheme scheme)
|
||||
{
|
||||
string gamePath = launchOptions.GamePath;
|
||||
string? directory = Path.GetDirectoryName(gamePath);
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
string configPath = Path.Combine(directory, ConfigFileName);
|
||||
|
||||
List<IniElement> elements = default!;
|
||||
try
|
||||
{
|
||||
using (FileStream readStream = File.OpenRead(configPath))
|
||||
{
|
||||
elements = [.. IniSerializer.Deserialize(readStream)];
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException ex)
|
||||
{
|
||||
ThrowHelper.GameFileOperation(SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath), ex);
|
||||
}
|
||||
catch (DirectoryNotFoundException ex)
|
||||
{
|
||||
ThrowHelper.GameFileOperation(SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath), ex);
|
||||
}
|
||||
catch (UnauthorizedAccessException ex)
|
||||
{
|
||||
ThrowHelper.GameFileOperation(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex);
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
|
||||
foreach (IniElement element in elements)
|
||||
{
|
||||
if (element is IniParameter parameter)
|
||||
{
|
||||
if (parameter.Key == "channel")
|
||||
{
|
||||
changed = parameter.Set(scheme.Channel.ToString("D")) || changed;
|
||||
}
|
||||
|
||||
if (parameter.Key == "sub_channel")
|
||||
{
|
||||
changed = parameter.Set(scheme.SubChannel.ToString("D")) || changed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
using (FileStream writeStream = File.Create(configPath))
|
||||
{
|
||||
IniSerializer.Serialize(writeStream, elements);
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,9 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Configuration;
|
||||
|
||||
internal interface IGameChannelOptionsService
|
||||
{
|
||||
ChannelOptions GetChannelOptions();
|
||||
|
||||
bool SetChannelOptions(LaunchScheme scheme);
|
||||
}
|
||||
@@ -9,38 +9,13 @@ namespace Snap.Hutao.Service.Game;
|
||||
[HighQuality]
|
||||
internal static class GameConstants
|
||||
{
|
||||
/// <summary>
|
||||
/// 设置文件
|
||||
/// </summary>
|
||||
public const string ConfigFileName = "config.ini";
|
||||
|
||||
/// <summary>
|
||||
/// 国服文件名
|
||||
/// </summary>
|
||||
public const string YuanShenFileName = "YuanShen.exe";
|
||||
|
||||
/// <summary>
|
||||
/// 外服文件名
|
||||
/// </summary>
|
||||
public const string YuanShenFileNameUpper = "YUANSHEN.EXE";
|
||||
public const string GenshinImpactFileName = "GenshinImpact.exe";
|
||||
|
||||
/// <summary>
|
||||
/// 国服数据文件夹
|
||||
/// </summary>
|
||||
public const string GenshinImpactFileNameUpper = "GENSHINIMPACT.EXE";
|
||||
public const string YuanShenData = "YuanShen_Data";
|
||||
|
||||
/// <summary>
|
||||
/// 国际服数据文件夹
|
||||
/// </summary>
|
||||
public const string GenshinImpactData = "GenshinImpact_Data";
|
||||
|
||||
/// <summary>
|
||||
/// 国服进程名
|
||||
/// </summary>
|
||||
public const string YuanShenProcessName = "YuanShen";
|
||||
|
||||
/// <summary>
|
||||
/// 外服进程名
|
||||
/// </summary>
|
||||
public const string GenshinImpactProcessName = "GenshinImpact";
|
||||
}
|
||||
@@ -2,12 +2,11 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.Service.Game.Account;
|
||||
using Snap.Hutao.Service.Game.Configuration;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using Snap.Hutao.Service.Game.Launching.Handler;
|
||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
using Snap.Hutao.Service.Game.Process;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
@@ -22,8 +21,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
{
|
||||
private readonly IGameChannelOptionsService gameChannelOptionsService;
|
||||
private readonly IGameAccountService gameAccountService;
|
||||
private readonly IGameProcessService gameProcessService;
|
||||
private readonly IGamePackageService gamePackageService;
|
||||
private readonly IGamePathService gamePathService;
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -45,27 +42,15 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetChannelOptions(LaunchScheme scheme)
|
||||
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
|
||||
{
|
||||
return gameChannelOptionsService.SetChannelOptions(scheme);
|
||||
return gameAccountService.DetectGameAccountAsync(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<GameAccount?> DetectGameAccountAsync()
|
||||
public GameAccount? DetectCurrentGameAccount(SchemeType scheme)
|
||||
{
|
||||
return gameAccountService.DetectGameAccountAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameAccount? DetectCurrentGameAccount()
|
||||
{
|
||||
return gameAccountService.DetectCurrentGameAccount();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool SetGameAccount(GameAccount account)
|
||||
{
|
||||
return gameAccountService.SetGameAccount(account);
|
||||
return gameAccountService.DetectCurrentGameAccount(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -89,18 +74,6 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
/// <inheritdoc/>
|
||||
public bool IsGameRunning()
|
||||
{
|
||||
return gameProcessService.IsGameRunning();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask LaunchAsync(IProgress<LaunchStatus> progress)
|
||||
{
|
||||
return gameProcessService.LaunchAsync(progress);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
return gamePackageService.EnsureGameResourceAsync(launchScheme, progress);
|
||||
return LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning(out _);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
internal static class GameServiceFacadeExtension
|
||||
{
|
||||
public static GameAccount? DetectCurrentGameAccount(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme)
|
||||
{
|
||||
return gameServiceFacade.DetectCurrentGameAccount(scheme.GetSchemeType());
|
||||
}
|
||||
|
||||
public static ValueTask<GameAccount?> DetectGameAccountAsync(this IGameServiceFacade gameServiceFacade, LaunchScheme scheme)
|
||||
{
|
||||
return gameServiceFacade.DetectGameAccountAsync(scheme.GetSchemeType());
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.Service.Game.Configuration;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
@@ -28,7 +29,7 @@ internal interface IGameServiceFacade
|
||||
/// <param name="uid">uid</param>
|
||||
void AttachGameAccountToUid(GameAccount gameAccount, string uid);
|
||||
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync();
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme);
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取游戏路径
|
||||
@@ -48,8 +49,6 @@ internal interface IGameServiceFacade
|
||||
/// <returns>是否正在运行</returns>
|
||||
bool IsGameRunning();
|
||||
|
||||
ValueTask LaunchAsync(IProgress<LaunchStatus> progress);
|
||||
|
||||
/// <summary>
|
||||
/// 异步修改游戏账号名称
|
||||
/// </summary>
|
||||
@@ -64,31 +63,5 @@ internal interface IGameServiceFacade
|
||||
/// <returns>任务</returns>
|
||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
/// <summary>
|
||||
/// 替换游戏资源
|
||||
/// </summary>
|
||||
/// <param name="launchScheme">目标启动方案</param>
|
||||
/// <param name="progress">进度</param>
|
||||
/// <returns>是否替换成功</returns>
|
||||
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
|
||||
|
||||
/// <summary>
|
||||
/// 修改注册表中的账号信息
|
||||
/// </summary>
|
||||
/// <param name="account">账号</param>
|
||||
/// <returns>是否设置成功</returns>
|
||||
bool SetGameAccount(GameAccount account);
|
||||
|
||||
/// <summary>
|
||||
/// 设置多通道值
|
||||
/// </summary>
|
||||
/// <param name="scheme">方案</param>
|
||||
/// <returns>是否更改了ini文件</returns>
|
||||
bool SetChannelOptions(LaunchScheme scheme);
|
||||
|
||||
/// <summary>
|
||||
/// 检测账号
|
||||
/// </summary>
|
||||
/// <returns>账号</returns>
|
||||
GameAccount? DetectCurrentGameAccount();
|
||||
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
|
||||
}
|
||||
@@ -42,6 +42,7 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
private NameValue<int>? monitor;
|
||||
private bool? isMonitorEnabled;
|
||||
private bool? isUseCloudThirdPartyMobile;
|
||||
private bool? isWindowsHDREnabled;
|
||||
private AspectRatio? selectedAspectRatio;
|
||||
private bool? useStarwardPlayTimeStatistics;
|
||||
private bool? setDiscordActivityWhenPlaying;
|
||||
@@ -198,6 +199,12 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
set => SetOption(ref isUseCloudThirdPartyMobile, SettingEntry.LaunchIsUseCloudThirdPartyMobile, value);
|
||||
}
|
||||
|
||||
public bool IsWindowsHDREnabled
|
||||
{
|
||||
get => GetOption(ref isWindowsHDREnabled, SettingEntry.LaunchIsWindowsHDREnabled, false);
|
||||
set => SetOption(ref isWindowsHDREnabled, SettingEntry.LaunchIsWindowsHDREnabled, value);
|
||||
}
|
||||
|
||||
public List<AspectRatio> AspectRatios { get; } =
|
||||
[
|
||||
new(2560, 1440),
|
||||
|
||||
@@ -9,12 +9,25 @@ namespace Snap.Hutao.Service.Game;
|
||||
|
||||
internal static class LaunchOptionsExtension
|
||||
{
|
||||
public static bool TryGetGameFolderAndFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameFolder, [NotNullWhen(true)] out string? gameFileName)
|
||||
public static bool TryGetGamePathAndGameDirectory(this LaunchOptions options, out string gamePath, [NotNullWhen(true)] out string? gameDirectory)
|
||||
{
|
||||
gamePath = options.GamePath;
|
||||
|
||||
gameDirectory = Path.GetDirectoryName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameDirectory))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetGameDirectoryAndGameFileName(this LaunchOptions options, [NotNullWhen(true)] out string? gameDirectory, [NotNullWhen(true)] out string? gameFileName)
|
||||
{
|
||||
string gamePath = options.GamePath;
|
||||
|
||||
gameFolder = Path.GetDirectoryName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameFolder))
|
||||
gameDirectory = Path.GetDirectoryName(gamePath);
|
||||
if (string.IsNullOrEmpty(gameDirectory))
|
||||
{
|
||||
gameFileName = default;
|
||||
return false;
|
||||
@@ -42,6 +55,18 @@ internal static class LaunchOptionsExtension
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryGetGamePathAndFilePathByName(this LaunchOptions options, string fileName, out string gamePath, [NotNullWhen(true)] out string? filePath)
|
||||
{
|
||||
if (options.TryGetGamePathAndGameDirectory(out gamePath, out string? gameDirectory))
|
||||
{
|
||||
filePath = Path.Combine(gameDirectory, fileName);
|
||||
return true;
|
||||
}
|
||||
|
||||
filePath = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static ImmutableList<GamePathEntry> GetGamePathEntries(this LaunchOptions options, out GamePathEntry? entry)
|
||||
{
|
||||
string gamePath = options.GamePath;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionEnsureGameNotRunningHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public static bool IsGameRunning([NotNullWhen(true)] out System.Diagnostics.Process? runningProcess)
|
||||
{
|
||||
// GetProcesses once and manually loop is O(n)
|
||||
foreach (ref readonly System.Diagnostics.Process process in System.Diagnostics.Process.GetProcesses().AsSpan())
|
||||
{
|
||||
if (string.Equals(process.ProcessName, GameConstants.YuanShenProcessName, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(process.ProcessName, GameConstants.GenshinImpactProcessName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
runningProcess = process;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
runningProcess = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (IsGameRunning(out System.Diagnostics.Process? process))
|
||||
{
|
||||
context.Logger.LogInformation("Game process detected, id: {Id}", process.Id);
|
||||
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameProcessRunning;
|
||||
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGameIsRunning;
|
||||
return;
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Snap.Hutao.Control.Extension;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
using Snap.Hutao.Service.Game.Package;
|
||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
using Snap.Hutao.View.Dialog;
|
||||
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionEnsureGameResourceHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
IServiceProvider serviceProvider = context.ServiceProvider;
|
||||
IContentDialogFactory contentDialogFactory = serviceProvider.GetRequiredService<IContentDialogFactory>();
|
||||
IProgressFactory progressFactory = serviceProvider.GetRequiredService<IProgressFactory>();
|
||||
|
||||
LaunchGamePackageConvertDialog dialog = await contentDialogFactory.CreateInstanceAsync<LaunchGamePackageConvertDialog>().ConfigureAwait(false);
|
||||
IProgress<PackageConvertStatus> convertProgress = progressFactory.CreateForMainThread<PackageConvertStatus>(state => dialog.State = state);
|
||||
|
||||
using (await dialog.BlockAsync(context.TaskContext).ConfigureAwait(false))
|
||||
{
|
||||
if (!await EnsureGameResourceAsync(context, convertProgress).ConfigureAwait(false))
|
||||
{
|
||||
// context.Result is set in EnsureGameResourceAsync
|
||||
return;
|
||||
}
|
||||
|
||||
await context.TaskContext.SwitchToMainThreadAsync();
|
||||
ImmutableList<GamePathEntry> gamePathEntries = context.Options.GetGamePathEntries(out GamePathEntry? selected);
|
||||
context.ViewModel.SetGamePathEntriesAndSelectedGamePathEntry(gamePathEntries, selected);
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async ValueTask<bool> EnsureGameResourceAsync(LaunchExecutionContext context, IProgress<PackageConvertStatus> progress)
|
||||
{
|
||||
if (!context.Options.TryGetGameDirectoryAndGameFileName(out string? gameFolder, out string? gameFileName))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
|
||||
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
|
||||
return false;
|
||||
}
|
||||
|
||||
context.Logger.LogInformation("Game folder: {GameFolder}", gameFolder);
|
||||
|
||||
if (!CheckDirectoryPermissions(gameFolder))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameDirectoryInsufficientPermissions;
|
||||
context.Result.ErrorMessage = SH.ServiceGameEnsureGameResourceInsufficientDirectoryPermissions;
|
||||
return false;
|
||||
}
|
||||
|
||||
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
|
||||
|
||||
ResourceClient resourceClient = context.ServiceProvider.GetRequiredService<ResourceClient>();
|
||||
Response<GameResource> response = await resourceClient.GetResourceAsync(context.Scheme).ConfigureAwait(false);
|
||||
|
||||
if (!response.TryGetDataWithoutUINotification(out GameResource? resource))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameResourceIndexQueryInvalidResponse;
|
||||
context.Result.ErrorMessage = SH.FormatServiceGameLaunchExecutionGameResourceQueryIndexFailed(response);
|
||||
return false;
|
||||
}
|
||||
|
||||
PackageConverter packageConverter = context.ServiceProvider.GetRequiredService<PackageConverter>();
|
||||
|
||||
if (!context.Scheme.ExecutableMatches(gameFileName))
|
||||
{
|
||||
if (!await packageConverter.EnsureGameResourceAsync(context.Scheme, resource, gameFolder, progress).ConfigureAwait(false))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameResourcePackageConvertInternalError;
|
||||
context.Result.ErrorMessage = SH.ViewModelLaunchGameEnsureGameResourceFail;
|
||||
return false;
|
||||
}
|
||||
|
||||
// We need to change the gamePath if we switched.
|
||||
string executableName = context.Scheme.IsOversea ? GameConstants.GenshinImpactFileName : GameConstants.YuanShenFileName;
|
||||
|
||||
await context.TaskContext.SwitchToMainThreadAsync();
|
||||
context.Options.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, executableName));
|
||||
}
|
||||
|
||||
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CheckDirectoryPermissions(string folder)
|
||||
{
|
||||
// Program Files has special permissions limitation.
|
||||
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
if (folder.StartsWith(programFiles, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string tempFilePath = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");
|
||||
string tempFilePathMove = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");
|
||||
|
||||
// Test create file
|
||||
using (SafeFileHandle handle = File.OpenHandle(tempFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, preallocationSize: 32 * 1024))
|
||||
{
|
||||
// Test write file
|
||||
RandomAccess.Write(handle, "SNAP HUTAO DIRECTORY PERMISSION CHECK"u8, 0);
|
||||
RandomAccess.FlushToDisk(handle);
|
||||
}
|
||||
|
||||
// Test move file
|
||||
File.Move(tempFilePath, tempFilePathMove);
|
||||
|
||||
// Test delete file
|
||||
File.Delete(tempFilePathMove);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionEnsureSchemeNotExistsHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (context.Scheme is null)
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.NoActiveScheme;
|
||||
context.Result.ErrorMessage = SH.ViewModelLaunchGameSchemeNotSelected;
|
||||
return;
|
||||
}
|
||||
|
||||
context.Logger.LogInformation("Scheme[{Scheme}] is selected", context.Scheme.DisplayName);
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionGameProcessExitHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (!context.Process.HasExited)
|
||||
{
|
||||
context.Progress.Report(new(LaunchPhase.WaitingForExit, SH.ServiceGameLaunchPhaseWaitingProcessExit));
|
||||
await context.Process.WaitForExitAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
context.Logger.LogInformation("Game process exited with code {ExitCode}", context.Process.ExitCode);
|
||||
context.Progress.Report(new(LaunchPhase.ProcessExited, SH.ServiceGameLaunchPhaseProcessExited));
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionGameProcessInitializationHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (!context.Options.TryGetGamePathAndGameFileName(out string gamePath, out string? gameFileName))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
|
||||
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
|
||||
return;
|
||||
}
|
||||
|
||||
context.Progress.Report(new(LaunchPhase.ProcessInitializing, SH.ServiceGameLaunchPhaseProcessInitializing));
|
||||
using (context.Process = InitializeGameProcess(context, gamePath))
|
||||
{
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static System.Diagnostics.Process InitializeGameProcess(LaunchExecutionContext context, string gamePath)
|
||||
{
|
||||
LaunchOptions launchOptions = context.Options;
|
||||
|
||||
string commandLine = string.Empty;
|
||||
if (launchOptions.IsEnabled)
|
||||
{
|
||||
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
|
||||
// https://docs.unity3d.com/2017.4/Documentation/Manual/CommandLineArguments.html
|
||||
commandLine = new CommandLineBuilder()
|
||||
.AppendIf(launchOptions.IsBorderless, "-popupwindow")
|
||||
.AppendIf(launchOptions.IsExclusive, "-window-mode", "exclusive")
|
||||
.Append("-screen-fullscreen", launchOptions.IsFullScreen ? 1 : 0)
|
||||
.AppendIf(launchOptions.IsScreenWidthEnabled, "-screen-width", launchOptions.ScreenWidth)
|
||||
.AppendIf(launchOptions.IsScreenHeightEnabled, "-screen-height", launchOptions.ScreenHeight)
|
||||
.AppendIf(launchOptions.IsMonitorEnabled, "-monitor", launchOptions.Monitor.Value)
|
||||
.AppendIf(launchOptions.IsUseCloudThirdPartyMobile, "-platform_type CLOUD_THIRD_PARTY_MOBILE")
|
||||
.ToString();
|
||||
}
|
||||
|
||||
context.Logger.LogInformation("Command Line Arguments: {commandLine}", commandLine);
|
||||
|
||||
return new()
|
||||
{
|
||||
StartInfo = new()
|
||||
{
|
||||
Arguments = commandLine,
|
||||
FileName = gamePath,
|
||||
UseShellExecute = true,
|
||||
Verb = "runas",
|
||||
WorkingDirectory = Path.GetDirectoryName(gamePath),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.Win32.Foundation;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionGameProcessStartHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
try
|
||||
{
|
||||
context.Process.Start();
|
||||
context.Logger.LogInformation("Process started");
|
||||
}
|
||||
catch (Win32Exception ex) when (ex.HResult == HRESULT.E_FAIL)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
context.Progress.Report(new(LaunchPhase.ProcessStarted, SH.ServiceGameLaunchPhaseProcessStarted));
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.IO.Ini;
|
||||
using Snap.Hutao.Service.Game.Configuration;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionSetChannelOptionsHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (!context.Options.TryGetGamePathAndFilePathByName(GameConstants.ConfigFileName, out string gamePath, out string? configPath))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.NoActiveGamePath;
|
||||
context.Result.ErrorMessage = SH.ServiceGameLaunchExecutionGamePathNotValid;
|
||||
return;
|
||||
}
|
||||
|
||||
context.Logger.LogInformation("Game config file path: {ConfigPath}", configPath);
|
||||
|
||||
List<IniElement> elements = default!;
|
||||
try
|
||||
{
|
||||
using (FileStream readStream = File.OpenRead(configPath))
|
||||
{
|
||||
elements = [.. IniSerializer.Deserialize(readStream)];
|
||||
}
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameConfigFileNotFound;
|
||||
context.Result.ErrorMessage = SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath);
|
||||
return;
|
||||
}
|
||||
catch (DirectoryNotFoundException)
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameConfigDirectoryNotFound;
|
||||
context.Result.ErrorMessage = SH.FormatServiceGameSetMultiChannelConfigFileNotFound(configPath);
|
||||
return;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameConfigInsufficientPermissions;
|
||||
context.Result.ErrorMessage = SH.ServiceGameSetMultiChannelUnauthorizedAccess;
|
||||
return;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
|
||||
foreach (IniElement element in elements)
|
||||
{
|
||||
if (element is IniParameter parameter)
|
||||
{
|
||||
if (parameter.Key is ChannelOptions.ChannelName)
|
||||
{
|
||||
changed = parameter.Set(context.Scheme.Channel.ToString("D")) || changed;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parameter.Key is ChannelOptions.SubChannelName)
|
||||
{
|
||||
changed = parameter.Set(context.Scheme.SubChannel.ToString("D")) || changed;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
using (FileStream writeStream = File.Create(configPath))
|
||||
{
|
||||
IniSerializer.Serialize(writeStream, elements);
|
||||
}
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Discord;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionSetDiscordActivityHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
bool previousSetDiscordActivityWhenPlaying = context.Options.SetDiscordActivityWhenPlaying;
|
||||
|
||||
try
|
||||
{
|
||||
if (previousSetDiscordActivityWhenPlaying)
|
||||
{
|
||||
context.Logger.LogInformation("Set discord activity as playing");
|
||||
await context.ServiceProvider
|
||||
.GetRequiredService<IDiscordService>()
|
||||
.SetPlayingActivityAsync(context.Scheme.IsOversea)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (previousSetDiscordActivityWhenPlaying)
|
||||
{
|
||||
context.Logger.LogInformation("Recover discord activity");
|
||||
await context.ServiceProvider
|
||||
.GetRequiredService<IDiscordService>()
|
||||
.SetNormalActivityAsync()
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Game.Account;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionSetGameAccountHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (context.Account is not null)
|
||||
{
|
||||
context.Logger.LogInformation("Set game account to [{Account}]", context.Account.Name);
|
||||
|
||||
if (!RegistryInterop.Set(context.Account))
|
||||
{
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameAccountRegistryWriteResultNotMatch;
|
||||
context.Result.ErrorMessage = SH.ViewModelLaunchGameSwitchGameAccountFail;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Game.Account;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionSetWindowsHDRHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (context.Options.IsWindowsHDREnabled)
|
||||
{
|
||||
context.Logger.LogInformation("Set Windows HDR");
|
||||
RegistryInterop.SetWindowsHDR(context.Scheme.IsOversea);
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Windows.System;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionStarwardPlayTimeStatisticsHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
if (context.Options.UseStarwardPlayTimeStatistics)
|
||||
{
|
||||
context.Logger.LogInformation("Using starward to count game time");
|
||||
await LaunchStarwardForPlayTimeStatisticsAsync(context).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async ValueTask LaunchStarwardForPlayTimeStatisticsAsync(LaunchExecutionContext context)
|
||||
{
|
||||
string gameBiz = context.Scheme.IsOversea ? "hk4e_global" : "hk4e_cn";
|
||||
Uri starwardPlayTimeUri = $"starward://playtime/{gameBiz}".ToUri();
|
||||
if (await Launcher.QueryUriSupportAsync(starwardPlayTimeUri, LaunchQuerySupportType.Uri) is LaunchQuerySupportStatus.Available)
|
||||
{
|
||||
context.Logger.LogInformation("Launching starward");
|
||||
await Launcher.LaunchUriAsync(starwardPlayTimeUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionStatusProgressHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService<IProgressFactory>();
|
||||
LaunchStatusOptions statusOptions = context.ServiceProvider.GetRequiredService<LaunchStatusOptions>();
|
||||
context.Progress = progressFactory.CreateForMainThread<LaunchStatus>(status => statusOptions.LaunchStatus = status);
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
using Snap.Hutao.Service.Game.Unlocker;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
internal sealed class LaunchExecutionUnlockFpsHandler : ILaunchExecutionDelegateHandler
|
||||
{
|
||||
public async ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next)
|
||||
{
|
||||
RuntimeOptions runtimeOptions = context.ServiceProvider.GetRequiredService<RuntimeOptions>();
|
||||
if (runtimeOptions.IsElevated && context.Options.IsAdvancedLaunchOptionsEnabled && context.Options.UnlockFps)
|
||||
{
|
||||
context.Logger.LogInformation("Unlocking FPS");
|
||||
context.Progress.Report(new(LaunchPhase.UnlockingFps, SH.ServiceGameLaunchPhaseUnlockingFps));
|
||||
|
||||
IProgressFactory progressFactory = context.ServiceProvider.GetRequiredService<IProgressFactory>();
|
||||
IProgress<UnlockerStatus> progress = progressFactory.CreateForMainThread<UnlockerStatus>(status => context.Progress.Report(LaunchStatus.FromUnlockStatus(status)));
|
||||
GameFpsUnlocker unlocker = context.ServiceProvider.CreateInstance<GameFpsUnlocker>(context.Process);
|
||||
|
||||
try
|
||||
{
|
||||
await unlocker.UnlockAsync(new(100, 20000, 3000), progress, context.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
context.Logger.LogCritical(ex, "Unlocking FPS failed");
|
||||
|
||||
context.Result.Kind = LaunchExecutionResultKind.GameFpsUnlockingFailed;
|
||||
context.Result.ErrorMessage = ex.Message;
|
||||
|
||||
// The Unlocker can't unlock the process
|
||||
context.Process.Kill();
|
||||
}
|
||||
}
|
||||
|
||||
await next().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching;
|
||||
|
||||
internal delegate ValueTask<LaunchExecutionContext> LaunchExecutionDelegate();
|
||||
|
||||
internal interface ILaunchExecutionDelegateHandler
|
||||
{
|
||||
ValueTask OnExecutionAsync(LaunchExecutionContext context, LaunchExecutionDelegate next);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.ViewModel.Game;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching;
|
||||
|
||||
[ConstructorGenerated]
|
||||
internal sealed partial class LaunchExecutionContext
|
||||
{
|
||||
private readonly ILogger<LaunchExecutionContext> logger;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly ITaskContext taskContext;
|
||||
private readonly LaunchOptions options;
|
||||
|
||||
[SuppressMessage("", "SH007")]
|
||||
public LaunchExecutionContext(IServiceProvider serviceProvider,IViewModelSupportLaunchExecution viewModel, LaunchScheme? scheme, GameAccount? account)
|
||||
: this(serviceProvider)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
Scheme = scheme!;
|
||||
Account = account;
|
||||
}
|
||||
|
||||
public LaunchExecutionResult Result { get; } = new();
|
||||
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
public IServiceProvider ServiceProvider { get => serviceProvider; }
|
||||
|
||||
public ITaskContext TaskContext { get => taskContext; }
|
||||
|
||||
public ILogger Logger { get => logger; }
|
||||
|
||||
public LaunchOptions Options { get => options; }
|
||||
|
||||
public IViewModelSupportLaunchExecution ViewModel { get; set; } = default!;
|
||||
|
||||
public LaunchScheme Scheme { get; set; } = default!;
|
||||
|
||||
public GameAccount? Account { get; set; }
|
||||
|
||||
public IProgress<LaunchStatus> Progress { get; set; } = default!;
|
||||
|
||||
public System.Diagnostics.Process Process { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching;
|
||||
|
||||
[Injection(InjectAs.Transient)]
|
||||
internal sealed class LaunchExecutionInvoker
|
||||
{
|
||||
private readonly Queue<ILaunchExecutionDelegateHandler> handlers;
|
||||
|
||||
public LaunchExecutionInvoker()
|
||||
{
|
||||
handlers = [];
|
||||
handlers.Enqueue(new LaunchExecutionEnsureGameNotRunningHandler());
|
||||
handlers.Enqueue(new LaunchExecutionEnsureSchemeNotExistsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionSetChannelOptionsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionEnsureGameResourceHandler());
|
||||
handlers.Enqueue(new LaunchExecutionSetGameAccountHandler());
|
||||
handlers.Enqueue(new LaunchExecutionSetWindowsHDRHandler());
|
||||
handlers.Enqueue(new LaunchExecutionStatusProgressHandler());
|
||||
handlers.Enqueue(new LaunchExecutionGameProcessInitializationHandler());
|
||||
handlers.Enqueue(new LaunchExecutionSetDiscordActivityHandler());
|
||||
handlers.Enqueue(new LaunchExecutionGameProcessStartHandler());
|
||||
handlers.Enqueue(new LaunchExecutionStarwardPlayTimeStatisticsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionUnlockFpsHandler());
|
||||
handlers.Enqueue(new LaunchExecutionGameProcessExitHandler());
|
||||
}
|
||||
|
||||
public async ValueTask<LaunchExecutionResult> InvokeAsync(LaunchExecutionContext context)
|
||||
{
|
||||
await InvokeHandlerAsync(context).ConfigureAwait(false);
|
||||
return context.Result;
|
||||
}
|
||||
|
||||
private async ValueTask<LaunchExecutionContext> InvokeHandlerAsync(LaunchExecutionContext context)
|
||||
{
|
||||
if (handlers.TryDequeue(out ILaunchExecutionDelegateHandler? handler))
|
||||
{
|
||||
string typeName = TypeNameHelper.GetTypeDisplayName(handler, false);
|
||||
context.Logger.LogInformation("Handler[{Handler}] begin execution", typeName);
|
||||
await handler.OnExecutionAsync(context, () => InvokeHandlerAsync(context)).ConfigureAwait(false);
|
||||
context.Logger.LogInformation("Handler[{Handler}] end execution", typeName);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching;
|
||||
|
||||
internal sealed class LaunchExecutionResult
|
||||
{
|
||||
public LaunchExecutionResultKind Kind { get; set; }
|
||||
|
||||
public string ErrorMessage { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching;
|
||||
|
||||
internal enum LaunchExecutionResultKind
|
||||
{
|
||||
Ok,
|
||||
NoActiveScheme,
|
||||
NoActiveGamePath,
|
||||
GameProcessRunning,
|
||||
GameConfigFileNotFound,
|
||||
GameConfigDirectoryNotFound,
|
||||
GameConfigInsufficientPermissions,
|
||||
GameDirectoryInsufficientPermissions,
|
||||
GameResourceIndexQueryInvalidResponse,
|
||||
GameResourcePackageConvertInternalError,
|
||||
GameAccountRegistryWriteResultNotMatch,
|
||||
GameFpsUnlockingFailed,
|
||||
}
|
||||
@@ -4,20 +4,13 @@
|
||||
namespace Snap.Hutao.Service.Game.Locator;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Transient, typeof(IGameLocatorFactory))]
|
||||
[Injection(InjectAs.Singleton, typeof(IGameLocatorFactory))]
|
||||
internal sealed partial class GameLocatorFactory : IGameLocatorFactory
|
||||
{
|
||||
[SuppressMessage("", "SH301")]
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
|
||||
public IGameLocator Create(GameLocationSource source)
|
||||
{
|
||||
return source switch
|
||||
{
|
||||
GameLocationSource.Registry => serviceProvider.GetRequiredService<RegistryLauncherLocator>(),
|
||||
GameLocationSource.UnityLog => serviceProvider.GetRequiredService<UnityLogGameLocator>(),
|
||||
GameLocationSource.Manual => serviceProvider.GetRequiredService<ManualGameLocator>(),
|
||||
_ => throw Must.NeverHappen(),
|
||||
};
|
||||
return serviceProvider.GetRequiredKeyedService<IGameLocator>(source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Locator;
|
||||
|
||||
internal static class GameLocatorFactoryExtensions
|
||||
{
|
||||
public static ValueTask<ValueResult<bool, string>> LocateAsync(this IGameLocatorFactory factory, GameLocationSource source)
|
||||
{
|
||||
return factory.Create(source).LocateGamePathAsync();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ namespace Snap.Hutao.Service.Game.Locator;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Transient)]
|
||||
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Manual)]
|
||||
internal sealed partial class ManualGameLocator : IGameLocator
|
||||
{
|
||||
private readonly IFileSystemPickerInteraction fileSystemPickerInteraction;
|
||||
@@ -26,7 +26,7 @@ internal sealed partial class ManualGameLocator : IGameLocator
|
||||
if (isPickerOk)
|
||||
{
|
||||
string fileName = System.IO.Path.GetFileName(file);
|
||||
if (fileName is GameConstants.YuanShenFileName or GameConstants.GenshinImpactFileName)
|
||||
if (fileName.ToUpperInvariant() is GameConstants.YuanShenFileNameUpper or GameConstants.GenshinImpactFileNameUpper)
|
||||
{
|
||||
return ValueTask.FromResult<ValueResult<bool, string>>(new(true, file));
|
||||
}
|
||||
|
||||
@@ -13,9 +13,10 @@ namespace Snap.Hutao.Service.Game.Locator;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Transient)]
|
||||
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.Registry)]
|
||||
internal sealed partial class RegistryLauncherLocator : IGameLocator
|
||||
{
|
||||
private const string RegistryKeyName = @"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神";
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -29,50 +30,37 @@ internal sealed partial class RegistryLauncherLocator : IGameLocator
|
||||
{
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
string? path = Path.GetDirectoryName(result.Value);
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
string configPath = Path.Combine(path, GameConstants.ConfigFileName);
|
||||
string? escapedPath;
|
||||
using (FileStream stream = File.OpenRead(configPath))
|
||||
{
|
||||
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream);
|
||||
escapedPath = elements
|
||||
.OfType<IniParameter>()
|
||||
.FirstOrDefault(p => p.Key == "game_install_path")?.Value;
|
||||
}
|
||||
|
||||
if (escapedPath is not null)
|
||||
{
|
||||
string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName);
|
||||
return new(true, gamePath);
|
||||
}
|
||||
string? path = Path.GetDirectoryName(result.Value);
|
||||
ArgumentException.ThrowIfNullOrEmpty(path);
|
||||
string configPath = Path.Combine(path, GameConstants.ConfigFileName);
|
||||
|
||||
string? escapedPath;
|
||||
using (FileStream stream = File.OpenRead(configPath))
|
||||
{
|
||||
IEnumerable<IniElement> elements = IniSerializer.Deserialize(stream);
|
||||
escapedPath = elements
|
||||
.OfType<IniParameter>()
|
||||
.FirstOrDefault(p => p.Key == "game_install_path")?.Value;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(escapedPath))
|
||||
{
|
||||
string gamePath = Path.Combine(Unescape(escapedPath), GameConstants.YuanShenFileName);
|
||||
return new(true, gamePath);
|
||||
}
|
||||
|
||||
return new(false, string.Empty);
|
||||
}
|
||||
|
||||
private static ValueResult<bool, string> LocateInternal(string key)
|
||||
private static ValueResult<bool, string> LocateInternal(string valueName)
|
||||
{
|
||||
using (RegistryKey? uninstallKey = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\原神"))
|
||||
if (Registry.GetValue(RegistryKeyName, valueName, null) is string path)
|
||||
{
|
||||
if (uninstallKey is not null)
|
||||
{
|
||||
if (uninstallKey.GetValue(key) is string path)
|
||||
{
|
||||
return new(true, path);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(false, default!);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(false, default!);
|
||||
}
|
||||
return new(true, path);
|
||||
}
|
||||
|
||||
return new(false, default!);
|
||||
}
|
||||
|
||||
private static string Unescape(string str)
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace Snap.Hutao.Service.Game.Locator;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Transient)]
|
||||
[Injection(InjectAs.Transient, typeof(IGameLocator), Key = GameLocationSource.UnityLog)]
|
||||
internal sealed partial class UnityLogGameLocator : IGameLocator
|
||||
{
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
|
||||
using Snap.Hutao.Web.Response;
|
||||
using System.IO;
|
||||
using static Snap.Hutao.Service.Game.GameConstants;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Package;
|
||||
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IGamePackageService))]
|
||||
internal sealed partial class GamePackageService : IGamePackageService
|
||||
{
|
||||
private readonly PackageConverter packageConverter;
|
||||
private readonly IServiceProvider serviceProvider;
|
||||
private readonly LaunchOptions launchOptions;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress)
|
||||
{
|
||||
if (!launchOptions.TryGetGameFolderAndFileName(out string? gameFolder, out string? gameFileName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CheckDirectoryPermissions(gameFolder))
|
||||
{
|
||||
progress.Report(new(SH.ServiceGameEnsureGameResourceInsufficientDirectoryPermissions));
|
||||
return false;
|
||||
}
|
||||
|
||||
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
|
||||
Response<GameResource> response = await serviceProvider
|
||||
.GetRequiredService<ResourceClient>()
|
||||
.GetResourceAsync(launchScheme)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!response.IsOk())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GameResource resource = response.Data;
|
||||
|
||||
if (!launchScheme.ExecutableMatches(gameFileName))
|
||||
{
|
||||
// We can't start the game
|
||||
// when we failed to convert game
|
||||
if (!await packageConverter.EnsureGameResourceAsync(launchScheme, resource, gameFolder, progress).ConfigureAwait(false))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We need to change the gamePath if we switched.
|
||||
string exeName = launchScheme.IsOversea ? GenshinImpactFileName : YuanShenFileName;
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
launchOptions.UpdateGamePathAndRefreshEntries(Path.Combine(gameFolder, exeName));
|
||||
}
|
||||
|
||||
await packageConverter.EnsureDeprecatedFilesAndSdkAsync(resource, gameFolder).ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CheckDirectoryPermissions(string folder)
|
||||
{
|
||||
try
|
||||
{
|
||||
string tempFilePath = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");
|
||||
string tempFilePathMove = Path.Combine(folder, $"{Guid.NewGuid():N}.tmp");
|
||||
|
||||
// Test create file
|
||||
using (SafeFileHandle handle = File.OpenHandle(tempFilePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, preallocationSize: 32 * 1024))
|
||||
{
|
||||
// Test write file
|
||||
RandomAccess.Write(handle, "SNAP HUTAO DIRECTORY PERMISSION CHECK"u8, 0);
|
||||
RandomAccess.FlushToDisk(handle);
|
||||
}
|
||||
|
||||
// Test move file
|
||||
File.Move(tempFilePath, tempFilePathMove);
|
||||
|
||||
// Test delete file
|
||||
File.Delete(tempFilePathMove);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Game.Scheme;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Package;
|
||||
|
||||
internal interface IGamePackageService
|
||||
{
|
||||
ValueTask<bool> EnsureGameResourceAsync(LaunchScheme launchScheme, IProgress<PackageReplaceStatus> progress);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using CommunityToolkit.Common;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Package;
|
||||
|
||||
/// <summary>
|
||||
/// 包更新状态
|
||||
/// </summary>
|
||||
internal sealed class PackageConvertStatus
|
||||
{
|
||||
public PackageConvertStatus(string name)
|
||||
{
|
||||
Name = name;
|
||||
Description = name;
|
||||
}
|
||||
|
||||
public PackageConvertStatus(string name, long bytesRead, long totalBytes)
|
||||
{
|
||||
Percent = (double)bytesRead / totalBytes;
|
||||
Name = name;
|
||||
Description = $"{Converters.ToFileSizeString(bytesRead)}/{Converters.ToFileSizeString(totalBytes)}";
|
||||
}
|
||||
|
||||
public string Name { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 描述
|
||||
/// </summary>
|
||||
public string Description { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// 进度
|
||||
/// </summary>
|
||||
public double Percent { get; set; } = -1;
|
||||
|
||||
/// <summary>
|
||||
/// 是否有进度
|
||||
/// </summary>
|
||||
public bool IsIndeterminate { get => Percent < 0; }
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using static Snap.Hutao.Service.Game.GameConstants;
|
||||
using RelativePathVersionItemDictionary = System.Collections.Generic.Dictionary<string, Snap.Hutao.Service.Game.Package.VersionItem>;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Package;
|
||||
|
||||
@@ -33,7 +34,7 @@ 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<PackageReplaceStatus> progress)
|
||||
public async ValueTask<bool> EnsureGameResourceAsync(LaunchScheme targetScheme, GameResource gameResource, string gameFolder, IProgress<PackageConvertStatus> progress)
|
||||
{
|
||||
// 以 国服 => 国际 为例
|
||||
// 1. 下载国际服的 pkg_version 文件,转换为索引字典
|
||||
@@ -58,15 +59,15 @@ internal sealed partial class PackageConverter
|
||||
string scatteredFilesUrl = gameResource.Game.Latest.DecompressedPath;
|
||||
string pkgVersionUrl = $"{scatteredFilesUrl}/{PackageVersion}";
|
||||
|
||||
PackageConvertContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
|
||||
PackageConverterFileSystemContext context = new(targetScheme.IsOversea, runtimeOptions.DataFolder, gameFolder, scatteredFilesUrl);
|
||||
|
||||
// Step 1
|
||||
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
|
||||
Dictionary<string, VersionItem> remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
|
||||
Dictionary<string, VersionItem> localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
|
||||
RelativePathVersionItemDictionary remoteItems = await GetRemoteItemsAsync(pkgVersionUrl).ConfigureAwait(false);
|
||||
RelativePathVersionItemDictionary localItems = await GetLocalItemsAsync(gameFolder).ConfigureAwait(false);
|
||||
|
||||
// Step 2
|
||||
List<ItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
|
||||
List<PackageItemOperationInfo> diffOperations = GetItemOperationInfos(remoteItems, localItems).ToList();
|
||||
diffOperations.SortBy(i => i.Type);
|
||||
|
||||
// Step 3
|
||||
@@ -116,16 +117,16 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<ItemOperationInfo> GetItemOperationInfos(Dictionary<string, VersionItem> remote, Dictionary<string, VersionItem> local)
|
||||
private static IEnumerable<PackageItemOperationInfo> GetItemOperationInfos(RelativePathVersionItemDictionary remote, RelativePathVersionItemDictionary local)
|
||||
{
|
||||
foreach ((string remoteName, VersionItem remoteItem) in remote)
|
||||
{
|
||||
if (local.TryGetValue(remoteName, out VersionItem? localItem))
|
||||
{
|
||||
if (!remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase))
|
||||
if (!(remoteItem.FileSize == localItem.FileSize && remoteItem.Md5.Equals(localItem.Md5, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
// 本地发现了同名且不同 MD5 的项,需要替换为服务器上的项
|
||||
yield return new(ItemOperationType.Replace, remoteItem, localItem);
|
||||
yield return new(PackageItemOperationType.Replace, remoteItem, localItem);
|
||||
}
|
||||
|
||||
// 同名同MD5,跳过
|
||||
@@ -134,22 +135,22 @@ internal sealed partial class PackageConverter
|
||||
else
|
||||
{
|
||||
// 本地没有发现同名项
|
||||
yield return new(ItemOperationType.Add, remoteItem, remoteItem);
|
||||
yield return new(PackageItemOperationType.Add, remoteItem, remoteItem);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ((_, VersionItem localItem) in local)
|
||||
{
|
||||
yield return new(ItemOperationType.Backup, localItem, localItem);
|
||||
yield return new(PackageItemOperationType.Backup, localItem, localItem);
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("^(?:YuanShen_Data|GenshinImpact_Data)(?=/)")]
|
||||
private static partial Regex DataFolderRegex();
|
||||
|
||||
private async ValueTask<Dictionary<string, VersionItem>> GetVersionItemsAsync(Stream stream)
|
||||
private async ValueTask<RelativePathVersionItemDictionary> GetVersionItemsAsync(Stream stream)
|
||||
{
|
||||
Dictionary<string, VersionItem> results = [];
|
||||
RelativePathVersionItemDictionary results = [];
|
||||
using (StreamReader reader = new(stream))
|
||||
{
|
||||
while (await reader.ReadLineAsync().ConfigureAwait(false) is { Length: > 0 } row)
|
||||
@@ -164,7 +165,7 @@ internal sealed partial class PackageConverter
|
||||
return results;
|
||||
}
|
||||
|
||||
private async ValueTask<Dictionary<string, VersionItem>> GetRemoteItemsAsync(string pkgVersionUrl)
|
||||
private async ValueTask<RelativePathVersionItemDictionary> GetRemoteItemsAsync(string pkgVersionUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -179,7 +180,7 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<Dictionary<string, VersionItem>> GetLocalItemsAsync(string gameFolder)
|
||||
private async ValueTask<RelativePathVersionItemDictionary> GetLocalItemsAsync(string gameFolder)
|
||||
{
|
||||
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
|
||||
{
|
||||
@@ -187,23 +188,23 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask PrepareCacheFilesAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
|
||||
private async ValueTask PrepareCacheFilesAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
|
||||
{
|
||||
foreach (ItemOperationInfo info in operations)
|
||||
foreach (PackageItemOperationInfo info in operations)
|
||||
{
|
||||
switch (info.Type)
|
||||
{
|
||||
case ItemOperationType.Backup:
|
||||
case PackageItemOperationType.Backup:
|
||||
continue;
|
||||
case ItemOperationType.Replace:
|
||||
case ItemOperationType.Add:
|
||||
case PackageItemOperationType.Replace:
|
||||
case PackageItemOperationType.Add:
|
||||
await SkipOrDownloadAsync(info, context, progress).ConfigureAwait(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask SkipOrDownloadAsync(ItemOperationInfo info, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
|
||||
private async ValueTask SkipOrDownloadAsync(PackageItemOperationInfo info, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
|
||||
{
|
||||
// 还原正确的远程地址
|
||||
string remoteName = string.Format(CultureInfo.CurrentCulture, info.Remote.RelativePath, context.ToDataFolderName);
|
||||
@@ -229,7 +230,7 @@ internal sealed partial class PackageConverter
|
||||
Directory.CreateDirectory(directory);
|
||||
|
||||
string remoteUrl = context.GetScatteredFilesUrl(remoteName);
|
||||
HttpShardCopyWorkerOptions<PackageReplaceStatus> options = new()
|
||||
HttpShardCopyWorkerOptions<PackageConvertStatus> options = new()
|
||||
{
|
||||
HttpClient = httpClient,
|
||||
SourceUrl = remoteUrl,
|
||||
@@ -237,7 +238,7 @@ internal sealed partial class PackageConverter
|
||||
StatusFactory = (bytesRead, totalBytes) => new(remoteName, bytesRead, totalBytes),
|
||||
};
|
||||
|
||||
using (HttpShardCopyWorker<PackageReplaceStatus> worker = await HttpShardCopyWorker<PackageReplaceStatus>.CreateAsync(options).ConfigureAwait(false))
|
||||
using (HttpShardCopyWorker<PackageConvertStatus> worker = await HttpShardCopyWorker<PackageConvertStatus>.CreateAsync(options).ConfigureAwait(false))
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -257,16 +258,16 @@ internal sealed partial class PackageConverter
|
||||
}
|
||||
}
|
||||
|
||||
private async ValueTask<bool> ReplaceGameResourceAsync(List<ItemOperationInfo> operations, PackageConvertContext context, IProgress<PackageReplaceStatus> progress)
|
||||
private async ValueTask<bool> ReplaceGameResourceAsync(List<PackageItemOperationInfo> operations, PackageConverterFileSystemContext context, IProgress<PackageConvertStatus> progress)
|
||||
{
|
||||
// 执行下载与移动操作
|
||||
foreach (ItemOperationInfo info in operations)
|
||||
foreach (PackageItemOperationInfo info in operations)
|
||||
{
|
||||
(bool moveToBackup, bool moveToTarget) = info.Type switch
|
||||
{
|
||||
ItemOperationType.Backup => (true, false),
|
||||
ItemOperationType.Replace => (true, true),
|
||||
ItemOperationType.Add => (false, true),
|
||||
PackageItemOperationType.Backup => (true, false),
|
||||
PackageItemOperationType.Replace => (true, true),
|
||||
PackageItemOperationType.Add => (false, true),
|
||||
_ => (false, false),
|
||||
};
|
||||
|
||||
@@ -321,7 +322,7 @@ internal sealed partial class PackageConverter
|
||||
return true;
|
||||
}
|
||||
|
||||
private async ValueTask ReplacePackageVersionFilesAsync(PackageConvertContext context)
|
||||
private async ValueTask ReplacePackageVersionFilesAsync(PackageConverterFileSystemContext context)
|
||||
{
|
||||
foreach (string versionFilePath in Directory.EnumerateFiles(context.GameFolder, "*pkg_version"))
|
||||
{
|
||||
|
||||
@@ -6,7 +6,7 @@ using static Snap.Hutao.Service.Game.GameConstants;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Package;
|
||||
|
||||
internal readonly struct PackageConvertContext
|
||||
internal readonly struct PackageConverterFileSystemContext
|
||||
{
|
||||
public readonly string GameFolder;
|
||||
public readonly string ServerCacheFolder;
|
||||
@@ -22,7 +22,7 @@ internal readonly struct PackageConvertContext
|
||||
public readonly string ScatteredFilesUrl;
|
||||
public readonly string PkgVersionUrl;
|
||||
|
||||
public PackageConvertContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl)
|
||||
public PackageConverterFileSystemContext(bool isTargetOversea, string dataFolder, string gameFolder, string scatteredFilesUrl)
|
||||
{
|
||||
GameFolder = gameFolder;
|
||||
ServerCacheFolder = Path.Combine(dataFolder, "ServerCache");
|
||||
@@ -37,7 +37,8 @@ internal readonly struct PackageConvertContext
|
||||
? (YuanShenData, GenshinImpactData)
|
||||
: (GenshinImpactData, YuanShenData);
|
||||
|
||||
(FromDataFolder, ToDataFolder) = (Path.Combine(GameFolder, FromDataFolderName), Path.Combine(GameFolder, ToDataFolderName));
|
||||
FromDataFolder = Path.Combine(GameFolder, FromDataFolderName);
|
||||
ToDataFolder = Path.Combine(GameFolder, ToDataFolderName);
|
||||
|
||||
ScatteredFilesUrl = scatteredFilesUrl;
|
||||
PkgVersionUrl = $"{scatteredFilesUrl}/pkg_version";
|
||||
@@ -10,12 +10,12 @@ namespace Snap.Hutao.Service.Game.Package;
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DebuggerDisplay("Action:{Type} Target:{Target} Cache:{Cache}")]
|
||||
internal readonly struct ItemOperationInfo
|
||||
internal readonly struct PackageItemOperationInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 操作的类型
|
||||
/// </summary>
|
||||
public readonly ItemOperationType Type;
|
||||
public readonly PackageItemOperationType Type;
|
||||
|
||||
/// <summary>
|
||||
/// 目标文件
|
||||
@@ -33,7 +33,7 @@ internal readonly struct ItemOperationInfo
|
||||
/// <param name="type">操作类型</param>
|
||||
/// <param name="remote">远程</param>
|
||||
/// <param name="local">本地</param>
|
||||
public ItemOperationInfo(ItemOperationType type, VersionItem remote, VersionItem local)
|
||||
public PackageItemOperationInfo(PackageItemOperationType type, VersionItem remote, VersionItem local)
|
||||
{
|
||||
Type = type;
|
||||
Remote = remote;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user