mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
18 Commits
1.10.7
...
advanced-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c261b7866 | ||
|
|
fc2d590c42 | ||
|
|
45724801ee | ||
|
|
4ef65a2811 | ||
|
|
e7bcc6e3ae | ||
|
|
618f55acbc | ||
|
|
c761d8b7ad | ||
|
|
b6a0592102 | ||
|
|
57e042ec1c | ||
|
|
36e5885ed6 | ||
|
|
599ddd147c | ||
|
|
04cd4e7137 | ||
|
|
f3934ce2cd | ||
|
|
8dd74c6c89 | ||
|
|
ebbaf0e36a | ||
|
|
78726cd2ea | ||
|
|
707fc67e51 | ||
|
|
fbe6abc63a |
@@ -89,6 +89,19 @@ public sealed class JsonSerializeTest
|
|||||||
Assert.AreEqual(result, """{"A":1,"B":2}""");
|
Assert.AreEqual(result, """{"A":1,"B":2}""");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void LowercaseStringCanDeserializeAsEnum()
|
||||||
|
{
|
||||||
|
string source = """
|
||||||
|
{
|
||||||
|
"Value": "a"
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
SampleClassHoldEnum sample = JsonSerializer.Deserialize<SampleClassHoldEnum>(source)!;
|
||||||
|
Assert.AreEqual(sample.Value, SampleEnum.A);
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class SampleDelegatePropertyClass
|
private sealed class SampleDelegatePropertyClass
|
||||||
{
|
{
|
||||||
public int A { get => B; set => B = value; }
|
public int A { get => B; set => B = value; }
|
||||||
@@ -118,6 +131,18 @@ public sealed class JsonSerializeTest
|
|||||||
public int B { get; set; }
|
public int B { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(JsonStringEnumConverter))]
|
||||||
|
private enum SampleEnum
|
||||||
|
{
|
||||||
|
A,
|
||||||
|
B,
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class SampleClassHoldEnum
|
||||||
|
{
|
||||||
|
public SampleEnum Value { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
[JsonDerivedType(typeof(SampleClassImplementedInterface))]
|
[JsonDerivedType(typeof(SampleClassImplementedInterface))]
|
||||||
private interface ISampleInterface
|
private interface ISampleInterface
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Test.IncomingFeature;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class UnlockerIslandFunctionOffsetTest
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions Options = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void GenerateJson()
|
||||||
|
{
|
||||||
|
UnlockerIslandConfigurationWrapper wrapper = new()
|
||||||
|
{
|
||||||
|
Oversea = new()
|
||||||
|
{
|
||||||
|
FunctionOffsetFieldOfView = 0x00000000_01688E60,
|
||||||
|
FunctionOffsetTargetFrameRate = 0x00000000_018834D0,
|
||||||
|
FunctionOffsetFog = 0x00000000_00FB2AD0,
|
||||||
|
},
|
||||||
|
Chinese = new()
|
||||||
|
{
|
||||||
|
FunctionOffsetFieldOfView = 0x00000000_01684560,
|
||||||
|
FunctionOffsetTargetFrameRate = 0x00000000_0187EBD0,
|
||||||
|
FunctionOffsetFog = 0x00000000_00FAE1D0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.WriteLine(JsonSerializer.Serialize(wrapper, Options));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UnlockerIslandConfigurationWrapper
|
||||||
|
{
|
||||||
|
public required UnlockerIslandConfiguration Oversea { get; set; }
|
||||||
|
|
||||||
|
public required UnlockerIslandConfiguration Chinese { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UnlockerIslandConfiguration
|
||||||
|
{
|
||||||
|
public required uint FunctionOffsetFieldOfView { get; set; }
|
||||||
|
|
||||||
|
public required uint FunctionOffsetTargetFrameRate { get; set; }
|
||||||
|
|
||||||
|
public required uint FunctionOffsetFog { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,8 +14,8 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
|
<PackageReference Include="MSTest.TestAdapter" Version="3.5.0" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
|
<PackageReference Include="MSTest.TestFramework" Version="3.5.0" />
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
15
src/Snap.Hutao/Snap.Hutao/Core/InstalledLocation.cs
Normal file
15
src/Snap.Hutao/Snap.Hutao/Core/InstalledLocation.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
using Windows.ApplicationModel;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core;
|
||||||
|
|
||||||
|
internal static class InstalledLocation
|
||||||
|
{
|
||||||
|
public static string GetAbsolutePath(string relativePath)
|
||||||
|
{
|
||||||
|
return Path.Combine(Package.Current.InstalledLocation.Path, relativePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,7 +30,7 @@ internal sealed class UnsafeEnumConverter<TEnum> : JsonConverter<TEnum>
|
|||||||
|
|
||||||
if (reader.GetString() is { } str)
|
if (reader.GetString() is { } str)
|
||||||
{
|
{
|
||||||
return Enum.Parse<TEnum>(str);
|
return Enum.Parse<TEnum>(str, ignoreCase: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new JsonException();
|
throw new JsonException();
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ internal sealed class RuntimeOptions
|
|||||||
});
|
});
|
||||||
|
|
||||||
private readonly LazySlim<string> lazyLocalCache = new(() => ApplicationData.Current.LocalCacheFolder.Path);
|
private readonly LazySlim<string> lazyLocalCache = new(() => ApplicationData.Current.LocalCacheFolder.Path);
|
||||||
private readonly LazySlim<string> lazyInstalledLocation = new(() => Package.Current.InstalledLocation.Path);
|
|
||||||
private readonly LazySlim<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
|
private readonly LazySlim<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
|
||||||
|
|
||||||
private readonly LazySlim<bool> lazyToastAvailable = new(() =>
|
private readonly LazySlim<bool> lazyToastAvailable = new(() =>
|
||||||
@@ -100,8 +99,6 @@ internal sealed class RuntimeOptions
|
|||||||
|
|
||||||
public string UserAgent { get => lazyVersionAndUserAgent.Value.UserAgent; }
|
public string UserAgent { get => lazyVersionAndUserAgent.Value.UserAgent; }
|
||||||
|
|
||||||
public string InstalledLocation { get => lazyInstalledLocation.Value; }
|
|
||||||
|
|
||||||
public string DataFolder { get => lazyDataFolder.Value; }
|
public string DataFolder { get => lazyDataFolder.Value; }
|
||||||
|
|
||||||
public string LocalCache { get => lazyLocalCache.Value; }
|
public string LocalCache { get => lazyLocalCache.Value; }
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ internal static class SettingKeys
|
|||||||
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
|
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
|
||||||
public const string OverridePackageConvertDirectoryPermissionsRequirement = "OverridePackageConvertDirectoryPermissionsRequirement";
|
public const string OverridePackageConvertDirectoryPermissionsRequirement = "OverridePackageConvertDirectoryPermissionsRequirement";
|
||||||
public const string AlwaysIsFirstRunAfterUpdate = "AlwaysIsFirstRunAfterUpdate";
|
public const string AlwaysIsFirstRunAfterUpdate = "AlwaysIsFirstRunAfterUpdate";
|
||||||
|
public const string AlphaBuildUseCNPatchEndpoint = "AlphaBuildUseCNPatchEndpoint";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Obsolete
|
#region Obsolete
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using Snap.Hutao.Win32.System.Com;
|
|||||||
using Snap.Hutao.Win32.UI.Shell;
|
using Snap.Hutao.Win32.UI.Shell;
|
||||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Windows.Storage;
|
|
||||||
using static Snap.Hutao.Win32.Macros;
|
using static Snap.Hutao.Win32.Macros;
|
||||||
using static Snap.Hutao.Win32.Ole32;
|
using static Snap.Hutao.Win32.Ole32;
|
||||||
|
|
||||||
@@ -18,27 +17,23 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
|||||||
{
|
{
|
||||||
private readonly RuntimeOptions runtimeOptions;
|
private readonly RuntimeOptions runtimeOptions;
|
||||||
|
|
||||||
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
|
public ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
|
||||||
{
|
{
|
||||||
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
|
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
|
||||||
string elevatedLauncherPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.Elevated.Launcher.exe");
|
string elevatedLauncherPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.Elevated.Launcher.exe");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
|
File.Copy(InstalledLocation.GetAbsolutePath("Assets/Logo.ico"), targetLogoPath, true);
|
||||||
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
|
File.Copy(InstalledLocation.GetAbsolutePath("Snap.Hutao.Elevated.Launcher.exe"), elevatedLauncherPath, true);
|
||||||
await iconFile.OverwriteCopyAsync(targetLogoPath).ConfigureAwait(false);
|
|
||||||
|
|
||||||
Uri elevatedLauncherUri = "ms-appx:///Snap.Hutao.Elevated.Launcher.exe".ToUri();
|
|
||||||
StorageFile launcherFile = await StorageFile.GetFileFromApplicationUriAsync(elevatedLauncherUri);
|
|
||||||
await launcherFile.OverwriteCopyAsync(elevatedLauncherPath).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
return false;
|
return ValueTask.FromResult(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath, elevatedLauncherPath);
|
bool result = UnsafeTryCreateDesktopShoutcutForElevatedLaunch(targetLogoPath, elevatedLauncherPath);
|
||||||
|
return ValueTask.FromResult(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath, string elevatedLauncherPath)
|
private unsafe bool UnsafeTryCreateDesktopShoutcutForElevatedLaunch(string targetLogoPath, string elevatedLauncherPath)
|
||||||
|
|||||||
6
src/Snap.Hutao/Snap.Hutao/Core/Void.cs
Normal file
6
src/Snap.Hutao/Snap.Hutao/Core/Void.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core;
|
||||||
|
|
||||||
|
internal readonly struct Void;
|
||||||
@@ -41,8 +41,9 @@ internal sealed partial class SettingEntry
|
|||||||
public const string LaunchScreenHeight = "Launch.ScreenHeight";
|
public const string LaunchScreenHeight = "Launch.ScreenHeight";
|
||||||
public const string LaunchIsScreenHeightEnabled = "Launch.IsScreenHeightEnabled";
|
public const string LaunchIsScreenHeightEnabled = "Launch.IsScreenHeightEnabled";
|
||||||
public const string LaunchUnlockFps = "Launch.UnlockFps";
|
public const string LaunchUnlockFps = "Launch.UnlockFps";
|
||||||
public const string LaunchUnlockerKind = "Launch.UnlockerKind";
|
|
||||||
public const string LaunchTargetFps = "Launch.TargetFps";
|
public const string LaunchTargetFps = "Launch.TargetFps";
|
||||||
|
public const string LaunchTargetFov = "Launch.TargetFov";
|
||||||
|
public const string LaunchDisableFog = "Launch.DisableFog";
|
||||||
public const string LaunchMonitor = "Launch.Monitor";
|
public const string LaunchMonitor = "Launch.Monitor";
|
||||||
public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
|
public const string LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
|
||||||
public const string LaunchIsUseCloudThirdPartyMobile = "Launch.IsUseCloudThirdPartyMobile";
|
public const string LaunchIsUseCloudThirdPartyMobile = "Launch.IsUseCloudThirdPartyMobile";
|
||||||
@@ -51,6 +52,9 @@ internal sealed partial class SettingEntry
|
|||||||
public const string LaunchUseBetterGenshinImpactAutomation = "Launch.UseBetterGenshinImpactAutomation";
|
public const string LaunchUseBetterGenshinImpactAutomation = "Launch.UseBetterGenshinImpactAutomation";
|
||||||
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
|
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
|
||||||
|
|
||||||
|
[Obsolete("不再区分解锁器类型,统一使用注入")]
|
||||||
|
public const string LaunchUnlockerKind = "Launch.UnlockerKind";
|
||||||
|
|
||||||
[Obsolete("不再支持多开")]
|
[Obsolete("不再支持多开")]
|
||||||
public const string MultipleInstances = "Launch.MultipleInstances";
|
public const string MultipleInstances = "Launch.MultipleInstances";
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ internal sealed class Hk4eItem : IMappingFrom<Hk4eItem, GachaItem>
|
|||||||
public required GachaType GachaType { get; set; }
|
public required GachaType GachaType { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("item_id")]
|
[JsonPropertyName("item_id")]
|
||||||
|
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||||
public required uint ItemId { get; set; }
|
public required uint ItemId { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("time")]
|
[JsonPropertyName("time")]
|
||||||
|
|||||||
@@ -12,4 +12,10 @@ internal static class LevelFormat
|
|||||||
{
|
{
|
||||||
return $"Lv.{value}";
|
return $"Lv.{value}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static string Format(uint value, uint extra)
|
||||||
|
{
|
||||||
|
return extra > 0 ? $"Lv.{value + extra} ({value} +{extra})" : $"Lv.{value}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1436,6 +1436,15 @@
|
|||||||
<data name="ViewDialogUpdatePackageDownloadUpdatelogLinkContent" xml:space="preserve">
|
<data name="ViewDialogUpdatePackageDownloadUpdatelogLinkContent" xml:space="preserve">
|
||||||
<value>查看更新日志</value>
|
<value>查看更新日志</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewDialogUpdatePackageMirrorDescription" xml:space="preserve">
|
||||||
|
<value>如果自动更新下载过慢,可以手动下载并安装</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewDialogUpdatePackageMirrorHeader" xml:space="preserve">
|
||||||
|
<value>下载源</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewDialogUpdatePackagePrimaryText" xml:space="preserve">
|
||||||
|
<value>更新</value>
|
||||||
|
</data>
|
||||||
<data name="ViewDialogUserDocumentAction" xml:space="preserve">
|
<data name="ViewDialogUserDocumentAction" xml:space="preserve">
|
||||||
<value>立即前往</value>
|
<value>立即前往</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -1535,6 +1544,9 @@
|
|||||||
<data name="ViewInfoBarPanelContractContent" xml:space="preserve">
|
<data name="ViewInfoBarPanelContractContent" xml:space="preserve">
|
||||||
<value>收起</value>
|
<value>收起</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewInfoBarPanelTitle" xml:space="preserve">
|
||||||
|
<value>所有通知</value>
|
||||||
|
</data>
|
||||||
<data name="ViewInfoBarToggleTitle" xml:space="preserve">
|
<data name="ViewInfoBarToggleTitle" xml:space="preserve">
|
||||||
<value>有新的通知</value>
|
<value>有新的通知</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2429,6 +2441,18 @@
|
|||||||
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
|
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
|
||||||
<value>所有选项仅会在启动游戏成功后保存</value>
|
<value>所有选项仅会在启动游戏成功后保存</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageLaunchGameDisableFogDescription" xml:space="preserve">
|
||||||
|
<value>移除光照渲染中的迷雾</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageLaunchGameDisableFogHeader" xml:space="preserve">
|
||||||
|
<value>移除迷雾</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageLaunchGameDisableFogOff" xml:space="preserve">
|
||||||
|
<value>保留</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageLaunchGameDisableFogOn" xml:space="preserve">
|
||||||
|
<value>移除</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageLaunchGameDiscordActivityDescription" xml:space="preserve">
|
<data name="ViewPageLaunchGameDiscordActivityDescription" xml:space="preserve">
|
||||||
<value>在我游戏时设置 Discord Activity 状态</value>
|
<value>在我游戏时设置 Discord Activity 状态</value>
|
||||||
</data>
|
</data>
|
||||||
@@ -2513,8 +2537,14 @@
|
|||||||
<data name="ViewPageLaunchGameSwitchSchemeWarning" xml:space="preserve">
|
<data name="ViewPageLaunchGameSwitchSchemeWarning" xml:space="preserve">
|
||||||
<value>版本更新前需要提前转换至与启动器匹配的服务器</value>
|
<value>版本更新前需要提前转换至与启动器匹配的服务器</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="ViewPageLaunchGameTargetFovDescription" xml:space="preserve">
|
||||||
|
<value>调整相机视野,默认 45</value>
|
||||||
|
</data>
|
||||||
|
<data name="ViewPageLaunchGameTargetFovHeader" xml:space="preserve">
|
||||||
|
<value>调整视野</value>
|
||||||
|
</data>
|
||||||
<data name="ViewPageLaunchGameUnlockFpsDescription" xml:space="preserve">
|
<data name="ViewPageLaunchGameUnlockFpsDescription" xml:space="preserve">
|
||||||
<value>请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率</value>
|
<value>请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率,将值设置为 -1 以表示无限制帧率</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="ViewPageLaunchGameUnlockFpsHeader" xml:space="preserve">
|
<data name="ViewPageLaunchGameUnlockFpsHeader" xml:space="preserve">
|
||||||
<value>解锁帧率限制</value>
|
<value>解锁帧率限制</value>
|
||||||
@@ -3245,6 +3275,9 @@
|
|||||||
<data name="WebDailyNoteResinRecoveryFormat" xml:space="preserve">
|
<data name="WebDailyNoteResinRecoveryFormat" xml:space="preserve">
|
||||||
<value>将于 {0} {1:HH:mm} 后全部恢复</value>
|
<value>将于 {0} {1:HH:mm} 后全部恢复</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="WebDailyNoteStoredAttendanceRefreshCountdown" xml:space="preserve">
|
||||||
|
<value>{0:c} 后重置</value>
|
||||||
|
</data>
|
||||||
<data name="WebDailyNoteTransformerAppend" xml:space="preserve">
|
<data name="WebDailyNoteTransformerAppend" xml:space="preserve">
|
||||||
<value>后可再次使用</value>
|
<value>后可再次使用</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ internal abstract partial class DbStoreOptions : ObservableObject
|
|||||||
return input.ToStringOrEmpty();
|
return input.ToStringOrEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void InitializeOptions(string keyLike, Expression<Func<SettingEntry, bool>> entrySelector, Action<string, string?> entryAction)
|
protected void InitializeOptions(Expression<Func<SettingEntry, bool>> entrySelector, Action<string, string?> entryAction)
|
||||||
{
|
{
|
||||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||||
{
|
{
|
||||||
@@ -109,6 +109,28 @@ internal abstract partial class DbStoreOptions : ObservableObject
|
|||||||
return storage.Value;
|
return storage.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected float GetOption(ref float? storage, string key, float defaultValue = 0f)
|
||||||
|
{
|
||||||
|
return GetOption(ref storage, key, () => defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected float GetOption(ref float? storage, string key, Func<float> defaultValueFactory)
|
||||||
|
{
|
||||||
|
if (storage is not null)
|
||||||
|
{
|
||||||
|
return storage.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||||
|
{
|
||||||
|
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
|
string? value = appDbContext.Settings.SingleOrDefault(e => e.Key == key)?.Value;
|
||||||
|
storage = value is null ? defaultValueFactory() : float.Parse(value, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return storage.Value;
|
||||||
|
}
|
||||||
|
|
||||||
[return: NotNullIfNotNull(nameof(defaultValue))]
|
[return: NotNullIfNotNull(nameof(defaultValue))]
|
||||||
protected T GetOption<T>(ref T? storage, string key, Func<string, T> deserializer, T defaultValue)
|
protected T GetOption<T>(ref T? storage, string key, Func<string, T> deserializer, T defaultValue)
|
||||||
{
|
{
|
||||||
@@ -132,59 +154,31 @@ internal abstract partial class DbStoreOptions : ObservableObject
|
|||||||
return storage;
|
return storage;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SetOption(ref string? storage, string key, string? value, [CallerMemberName] string? propertyName = null)
|
protected bool SetOption(ref string? storage, string key, string? value, [CallerMemberName] string? propertyName = null)
|
||||||
{
|
{
|
||||||
if (!SetProperty(ref storage, value, propertyName))
|
return SetOption(ref storage, key, value, v => v, propertyName);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
|
||||||
{
|
|
||||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
||||||
appDbContext.Settings.Where(e => e.Key == key).ExecuteDelete();
|
|
||||||
appDbContext.Settings.AddAndSave(new(key, value));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool SetOption(ref bool? storage, string key, bool value, [CallerMemberName] string? propertyName = null)
|
protected bool SetOption(ref bool? storage, string key, bool value, [CallerMemberName] string? propertyName = null)
|
||||||
{
|
{
|
||||||
bool set = SetProperty(ref storage, value, propertyName);
|
return SetOption(ref storage, key, value, v => $"{v}", propertyName);
|
||||||
if (!set)
|
|
||||||
{
|
|
||||||
return set;
|
|
||||||
}
|
|
||||||
|
|
||||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
|
||||||
{
|
|
||||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
||||||
appDbContext.Settings.Where(e => e.Key == key).ExecuteDelete();
|
|
||||||
appDbContext.Settings.AddAndSave(new(key, value.ToString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return set;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SetOption(ref int? storage, string key, int value, [CallerMemberName] string? propertyName = null)
|
protected bool SetOption(ref int? storage, string key, int value, [CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
return SetOption(ref storage, key, value, v => $"{v}", propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool SetOption(ref float? storage, string key, float value, [CallerMemberName] string? propertyName = null)
|
||||||
|
{
|
||||||
|
return SetOption(ref storage, key, value, v => $"{v}", propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool SetOption<T>(ref T? storage, string key, T value, Func<T, string?> serializer, [CallerMemberName] string? propertyName = null)
|
||||||
{
|
{
|
||||||
if (!SetProperty(ref storage, value, propertyName))
|
if (!SetProperty(ref storage, value, propertyName))
|
||||||
{
|
{
|
||||||
return;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
|
||||||
{
|
|
||||||
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
||||||
appDbContext.Settings.Where(e => e.Key == key).ExecuteDelete();
|
|
||||||
appDbContext.Settings.AddAndSave(new(key, $"{value}"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void SetOption<T>(ref T? storage, string key, T value, Func<T, string> serializer, [CallerMemberName] string? propertyName = null)
|
|
||||||
{
|
|
||||||
if (!SetProperty(ref storage, value, propertyName))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
using (IServiceScope scope = serviceProvider.CreateScope())
|
||||||
@@ -193,5 +187,7 @@ internal abstract partial class DbStoreOptions : ObservableObject
|
|||||||
appDbContext.Settings.Where(e => e.Key == key).ExecuteDelete();
|
appDbContext.Settings.Where(e => e.Key == key).ExecuteDelete();
|
||||||
appDbContext.Settings.AddAndSave(new(key, serializer(value)));
|
appDbContext.Settings.AddAndSave(new(key, serializer(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ using Snap.Hutao.Model.Metadata.Avatar;
|
|||||||
using Snap.Hutao.Model.Metadata.Converter;
|
using Snap.Hutao.Model.Metadata.Converter;
|
||||||
using Snap.Hutao.Model.Primitive;
|
using Snap.Hutao.Model.Primitive;
|
||||||
using Snap.Hutao.ViewModel.AvatarProperty;
|
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
|
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
|
||||||
|
|
||||||
@@ -199,30 +200,29 @@ internal static class AvatarViewBuilderExtension
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
IReadOnlyDictionary<SkillGroupId, SkillLevel> extraLevelMap = proudSkillExtraLevelMap as IReadOnlyDictionary<SkillGroupId, SkillLevel> ?? ImmutableDictionary<SkillGroupId, SkillLevel>.Empty;
|
||||||
Dictionary<SkillId, SkillLevel> skillExtraLeveledMap = new(skillLevelMap);
|
Dictionary<SkillId, SkillLevel> skillExtraLeveledMap = new(skillLevelMap);
|
||||||
|
|
||||||
if (proudSkillExtraLevelMap is not null)
|
foreach ((SkillGroupId groupId, SkillLevel extraLevel) in extraLevelMap)
|
||||||
{
|
{
|
||||||
foreach ((SkillGroupId groupId, SkillLevel extraLevel) in proudSkillExtraLevelMap)
|
skillExtraLeveledMap.IncreaseByValue(proudSkills.Single(p => p.GroupId == groupId).Id, extraLevel);
|
||||||
{
|
|
||||||
skillExtraLeveledMap.IncreaseByValue(proudSkills.Single(p => p.GroupId == groupId).Id, extraLevel);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return proudSkills.SelectList(proudableSkill =>
|
return proudSkills.SelectList(proudSkill =>
|
||||||
{
|
{
|
||||||
SkillId skillId = proudableSkill.Id;
|
SkillId skillId = proudSkill.Id;
|
||||||
|
|
||||||
// TODO: use builder here
|
// TODO: use builder here
|
||||||
return new SkillView()
|
return new SkillView()
|
||||||
{
|
{
|
||||||
Name = proudableSkill.Name,
|
Name = proudSkill.Name,
|
||||||
Icon = SkillIconConverter.IconNameToUri(proudableSkill.Icon),
|
Icon = SkillIconConverter.IconNameToUri(proudSkill.Icon),
|
||||||
Description = proudableSkill.Description,
|
Description = proudSkill.Description,
|
||||||
|
|
||||||
GroupId = proudableSkill.GroupId,
|
GroupId = proudSkill.GroupId,
|
||||||
|
Level = LevelFormat.Format(skillLevelMap[skillId], extraLevelMap.GetValueOrDefault(proudSkill.GroupId)),
|
||||||
LevelNumber = skillLevelMap[skillId],
|
LevelNumber = skillLevelMap[skillId],
|
||||||
Info = DescriptionsParametersDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[skillId]),
|
Info = DescriptionsParametersDescriptor.Convert(proudSkill.Proud, skillExtraLeveledMap[skillId]),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
37
src/Snap.Hutao/Snap.Hutao/Service/Feature/FeatureService.cs
Normal file
37
src/Snap.Hutao/Snap.Hutao/Service/Feature/FeatureService.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||||
|
using Snap.Hutao.Service.Game.Unlocker.Island;
|
||||||
|
using Snap.Hutao.Web;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Feature;
|
||||||
|
|
||||||
|
[ConstructorGenerated]
|
||||||
|
[Injection(InjectAs.Singleton, typeof(IFeatureService))]
|
||||||
|
[HttpClient(HttpClientConfiguration.Default)]
|
||||||
|
internal sealed partial class FeatureService : IFeatureService
|
||||||
|
{
|
||||||
|
private readonly IServiceScopeFactory serviceScopeFactory;
|
||||||
|
|
||||||
|
public async ValueTask<IslandFeature?> GetIslandFeatureAsync()
|
||||||
|
{
|
||||||
|
using (IServiceScope scope = serviceScopeFactory.CreateScope())
|
||||||
|
{
|
||||||
|
IHttpClientFactory httpClientFactory = scope.ServiceProvider.GetRequiredService<IHttpClientFactory>();
|
||||||
|
using (HttpClient httpClient = httpClientFactory.CreateClient(nameof(FeatureService)))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await httpClient.GetFromJsonAsync<IslandFeature>(HutaoEndpoints.Feature("UnlockerIsland")).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Snap.Hutao/Snap.Hutao/Service/Feature/IFeatureService.cs
Normal file
11
src/Snap.Hutao/Snap.Hutao/Service/Feature/IFeatureService.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Service.Game.Unlocker.Island;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Feature;
|
||||||
|
|
||||||
|
internal interface IFeatureService
|
||||||
|
{
|
||||||
|
ValueTask<IslandFeature?> GetIslandFeatureAsync();
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ using Snap.Hutao.Model;
|
|||||||
using Snap.Hutao.Model.Entity;
|
using Snap.Hutao.Model.Entity;
|
||||||
using Snap.Hutao.Service.Abstraction;
|
using Snap.Hutao.Service.Abstraction;
|
||||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||||
using Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
using Snap.Hutao.Win32.Graphics.Gdi;
|
using Snap.Hutao.Win32.Graphics.Gdi;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
@@ -39,8 +38,9 @@ internal sealed class LaunchOptions : DbStoreOptions
|
|||||||
private int? screenHeight;
|
private int? screenHeight;
|
||||||
private bool? isScreenHeightEnabled;
|
private bool? isScreenHeightEnabled;
|
||||||
private bool? unlockFps;
|
private bool? unlockFps;
|
||||||
private NameDescriptionValue<GameFpsUnlockerKind>? unlockerKind;
|
|
||||||
private int? targetFps;
|
private int? targetFps;
|
||||||
|
private float? targetFov;
|
||||||
|
private bool? disableFog;
|
||||||
private NameValue<int>? monitor;
|
private NameValue<int>? monitor;
|
||||||
private bool? isMonitorEnabled;
|
private bool? isMonitorEnabled;
|
||||||
private bool? isUseCloudThirdPartyMobile;
|
private bool? isUseCloudThirdPartyMobile;
|
||||||
@@ -60,6 +60,63 @@ internal sealed class LaunchOptions : DbStoreOptions
|
|||||||
InitializeMonitors(Monitors);
|
InitializeMonitors(Monitors);
|
||||||
InitializeScreenFps(out primaryScreenFps);
|
InitializeScreenFps(out primaryScreenFps);
|
||||||
|
|
||||||
|
// Batch initialization, boost up performance
|
||||||
|
InitializeOptions(entry => entry.Key.StartsWith("Launch."), (key, value) =>
|
||||||
|
{
|
||||||
|
_ = key switch
|
||||||
|
{
|
||||||
|
SettingEntry.LaunchIsLaunchOptionsEnabled => InitializeBooleanValue(ref isEnabled, value),
|
||||||
|
SettingEntry.LaunchIsFullScreen => InitializeBooleanValue(ref isFullScreen, value),
|
||||||
|
SettingEntry.LaunchIsBorderless => InitializeBooleanValue(ref isBorderless, value),
|
||||||
|
SettingEntry.LaunchIsExclusive => InitializeBooleanValue(ref isExclusive, value),
|
||||||
|
SettingEntry.LaunchScreenWidth => InitializeInt32Value(ref screenWidth, value),
|
||||||
|
SettingEntry.LaunchIsScreenWidthEnabled => InitializeBooleanValue(ref isScreenWidthEnabled, value),
|
||||||
|
SettingEntry.LaunchScreenHeight => InitializeInt32Value(ref screenHeight, value),
|
||||||
|
SettingEntry.LaunchIsScreenHeightEnabled => InitializeBooleanValue(ref isScreenHeightEnabled, value),
|
||||||
|
SettingEntry.LaunchUnlockFps => InitializeBooleanValue(ref unlockFps, value),
|
||||||
|
SettingEntry.LaunchTargetFps => InitializeInt32Value(ref targetFps, value),
|
||||||
|
SettingEntry.LaunchTargetFov => InitializeFloatValue(ref targetFov, value),
|
||||||
|
SettingEntry.LaunchDisableFog => InitializeBooleanValue(ref disableFog, value),
|
||||||
|
SettingEntry.LaunchIsMonitorEnabled => InitializeBooleanValue(ref isMonitorEnabled, value),
|
||||||
|
SettingEntry.LaunchIsUseCloudThirdPartyMobile => InitializeBooleanValue(ref isUseCloudThirdPartyMobile, value),
|
||||||
|
SettingEntry.LaunchIsWindowsHDREnabled => InitializeBooleanValue(ref isWindowsHDREnabled, value),
|
||||||
|
SettingEntry.LaunchUseStarwardPlayTimeStatistics => InitializeBooleanValue(ref useStarwardPlayTimeStatistics, value),
|
||||||
|
SettingEntry.LaunchUseBetterGenshinImpactAutomation => InitializeBooleanValue(ref useBetterGenshinImpactAutomation, value),
|
||||||
|
SettingEntry.LaunchSetDiscordActivityWhenPlaying => InitializeBooleanValue(ref setDiscordActivityWhenPlaying, value),
|
||||||
|
_ => default,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
static Core.Void InitializeBooleanValue(ref bool? storage, string? value)
|
||||||
|
{
|
||||||
|
if (value is not null)
|
||||||
|
{
|
||||||
|
storage = bool.Parse(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Core.Void InitializeInt32Value(ref int? storage, string? value)
|
||||||
|
{
|
||||||
|
if (value is not null)
|
||||||
|
{
|
||||||
|
storage = int.Parse(value, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Core.Void InitializeFloatValue(ref float? storage, string? value)
|
||||||
|
{
|
||||||
|
if (value is not null)
|
||||||
|
{
|
||||||
|
storage = float.Parse(value, CultureInfo.InvariantCulture);
|
||||||
|
}
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
static void InitializeMonitors(List<NameValue<int>> monitors)
|
static void InitializeMonitors(List<NameValue<int>> monitors)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -109,21 +166,22 @@ internal sealed class LaunchOptions : DbStoreOptions
|
|||||||
set => SetOption(ref gamePathEntries, SettingEntry.GamePathEntries, value, value => JsonSerializer.Serialize(value));
|
set => SetOption(ref gamePathEntries, SettingEntry.GamePathEntries, value, value => JsonSerializer.Serialize(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsEnabled
|
|
||||||
{
|
|
||||||
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, false);
|
|
||||||
set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsAdvancedLaunchOptionsEnabled
|
public bool IsAdvancedLaunchOptionsEnabled
|
||||||
{
|
{
|
||||||
get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
|
get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
|
||||||
set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value);
|
set => SetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Launch Prefixed Options
|
||||||
|
public bool IsEnabled
|
||||||
|
{
|
||||||
|
get => GetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, true);
|
||||||
|
set => SetOption(ref isEnabled, SettingEntry.LaunchIsLaunchOptionsEnabled, value);
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsFullScreen
|
public bool IsFullScreen
|
||||||
{
|
{
|
||||||
get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen);
|
get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen, false);
|
||||||
set => SetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen, value);
|
set => SetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,44 +227,25 @@ internal sealed class LaunchOptions : DbStoreOptions
|
|||||||
set => SetOption(ref unlockFps, SettingEntry.LaunchUnlockFps, value);
|
set => SetOption(ref unlockFps, SettingEntry.LaunchUnlockFps, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NameDescriptionValue<GameFpsUnlockerKind>> UnlockerKinds { get; } =
|
|
||||||
[
|
|
||||||
new(SH.ServiceGameLaunchUnlockerKindLegacyName, SH.ServiceGameLaunchUnlockerKindLegacyDescription, GameFpsUnlockerKind.Legacy),
|
|
||||||
new(SH.ServiceGameLaunchUnlockerKindIslandName, SH.ServiceGameLaunchUnlockerKindIslandDescription, GameFpsUnlockerKind.Island),
|
|
||||||
];
|
|
||||||
|
|
||||||
public NameDescriptionValue<GameFpsUnlockerKind> UnlockerKind
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
return GetOption(ref unlockerKind, SettingEntry.LaunchUnlockerKind, name => GetKind(name, UnlockerKinds), UnlockerKinds[0]);
|
|
||||||
|
|
||||||
static NameDescriptionValue<GameFpsUnlockerKind> GetKind(string name, List<NameDescriptionValue<GameFpsUnlockerKind>> unlockerKinds)
|
|
||||||
{
|
|
||||||
GameFpsUnlockerKind kind = Enum.Parse<GameFpsUnlockerKind>(name);
|
|
||||||
return unlockerKinds.Single(entry => entry.Value == kind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (value is not null)
|
|
||||||
{
|
|
||||||
SetOption(ref unlockerKind, SettingEntry.LaunchUnlockerKind, value, selected => selected.Value.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int TargetFps
|
public int TargetFps
|
||||||
{
|
{
|
||||||
get => GetOption(ref targetFps, SettingEntry.LaunchTargetFps, primaryScreenFps);
|
get => GetOption(ref targetFps, SettingEntry.LaunchTargetFps, primaryScreenFps);
|
||||||
set => SetOption(ref targetFps, SettingEntry.LaunchTargetFps, value);
|
set => SetOption(ref targetFps, SettingEntry.LaunchTargetFps, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<NameValue<int>> Monitors { get; } = [];
|
public float TargetFov
|
||||||
|
{
|
||||||
|
get => GetOption(ref targetFov, SettingEntry.LaunchTargetFov, 45f);
|
||||||
|
set => SetOption(ref targetFov, SettingEntry.LaunchTargetFov, value);
|
||||||
|
}
|
||||||
|
|
||||||
[AllowNull]
|
public bool DisableFog
|
||||||
public NameValue<int> Monitor
|
{
|
||||||
|
get => GetOption(ref disableFog, SettingEntry.LaunchDisableFog, false);
|
||||||
|
set => SetOption(ref disableFog, SettingEntry.LaunchDisableFog, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NameValue<int>? Monitor
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@@ -245,6 +284,27 @@ internal sealed class LaunchOptions : DbStoreOptions
|
|||||||
set => SetOption(ref isWindowsHDREnabled, SettingEntry.LaunchIsWindowsHDREnabled, value);
|
set => SetOption(ref isWindowsHDREnabled, SettingEntry.LaunchIsWindowsHDREnabled, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool UseStarwardPlayTimeStatistics
|
||||||
|
{
|
||||||
|
get => GetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, false);
|
||||||
|
set => SetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool UseBetterGenshinImpactAutomation
|
||||||
|
{
|
||||||
|
get => GetOption(ref useBetterGenshinImpactAutomation, SettingEntry.LaunchUseBetterGenshinImpactAutomation, false);
|
||||||
|
set => SetOption(ref useBetterGenshinImpactAutomation, SettingEntry.LaunchUseBetterGenshinImpactAutomation, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool SetDiscordActivityWhenPlaying
|
||||||
|
{
|
||||||
|
get => GetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, true);
|
||||||
|
set => SetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, value);
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
public List<NameValue<int>> Monitors { get; } = [];
|
||||||
|
|
||||||
public List<AspectRatio> AspectRatios { get; } =
|
public List<AspectRatio> AspectRatios { get; } =
|
||||||
[
|
[
|
||||||
new(3840, 2160),
|
new(3840, 2160),
|
||||||
@@ -265,22 +325,4 @@ internal sealed class LaunchOptions : DbStoreOptions
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool UseStarwardPlayTimeStatistics
|
|
||||||
{
|
|
||||||
get => GetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, false);
|
|
||||||
set => SetOption(ref useStarwardPlayTimeStatistics, SettingEntry.LaunchUseStarwardPlayTimeStatistics, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool UseBetterGenshinImpactAutomation
|
|
||||||
{
|
|
||||||
get => GetOption(ref useBetterGenshinImpactAutomation, SettingEntry.LaunchUseBetterGenshinImpactAutomation, false);
|
|
||||||
set => SetOption(ref useBetterGenshinImpactAutomation, SettingEntry.LaunchUseBetterGenshinImpactAutomation, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool SetDiscordActivityWhenPlaying
|
|
||||||
{
|
|
||||||
get => GetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, true);
|
|
||||||
set => SetOption(ref setDiscordActivityWhenPlaying, SettingEntry.LaunchSetDiscordActivityWhenPlaying, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -19,13 +19,6 @@ internal sealed class LaunchStatus
|
|||||||
|
|
||||||
public static LaunchStatus FromUnlockerContext(GameFpsUnlockerContext unlockerState)
|
public static LaunchStatus FromUnlockerContext(GameFpsUnlockerContext unlockerState)
|
||||||
{
|
{
|
||||||
if (unlockerState.FindModuleResult == FindModuleResult.Ok)
|
return new(LaunchPhase.UnlockFpsSucceed, unlockerState.Description ?? SH.ServiceGameLaunchPhaseUnlockFpsSucceed);
|
||||||
{
|
|
||||||
return new(LaunchPhase.UnlockFpsSucceed, unlockerState.Description ?? SH.ServiceGameLaunchPhaseUnlockFpsSucceed);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return new(LaunchPhase.UnlockFpsFailed, unlockerState.Description ?? SH.ServiceGameLaunchPhaseUnlockFpsFailed);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
using Snap.Hutao.Factory.Progress;
|
using Snap.Hutao.Factory.Progress;
|
||||||
using Snap.Hutao.Service.Game.Unlocker;
|
using Snap.Hutao.Service.Game.Unlocker;
|
||||||
using Snap.Hutao.Service.Game.Unlocker.Island;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||||
|
|
||||||
@@ -25,12 +24,7 @@ internal sealed class LaunchExecutionUnlockFpsHandler : ILaunchExecutionDelegate
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
UnlockOptions unlockOptions = new(gameFileSystem, 100, 20000, 2000);
|
GameFpsUnlocker unlocker = new(context.ServiceProvider, context.Process, progress);
|
||||||
IGameFpsUnlocker unlocker = context.Options.UnlockerKind.Value switch
|
|
||||||
{
|
|
||||||
GameFpsUnlockerKind.Island => new IslandGameFpsUnlocker(context.ServiceProvider, context.Process, unlockOptions, progress),
|
|
||||||
_ => new DefaultGameFpsUnlocker(context.ServiceProvider, context.Process, unlockOptions, progress),
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Win32.Foundation;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using static Snap.Hutao.Win32.Kernel32;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
internal sealed class DefaultGameFpsUnlocker : GameFpsUnlocker
|
|
||||||
{
|
|
||||||
public DefaultGameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, in UnlockOptions options, IProgress<GameFpsUnlockerContext> progress)
|
|
||||||
: base(serviceProvider, gameProcess, options, progress)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async ValueTask PostUnlockOverrideAsync(GameFpsUnlockerContext context, LaunchOptions launchOptions, ILogger logger, CancellationToken token = default)
|
|
||||||
{
|
|
||||||
using (PeriodicTimer timer = new(context.Options.AdjustFpsDelay))
|
|
||||||
{
|
|
||||||
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
if (!context.GameProcess.HasExited && context.FpsAddress != 0U)
|
|
||||||
{
|
|
||||||
UnsafeWriteProcessMemory(context.AllAccess, context.FpsAddress, launchOptions.TargetFps);
|
|
||||||
WIN32_ERROR error = GetLastError();
|
|
||||||
if (error is not WIN32_ERROR.NO_ERROR)
|
|
||||||
{
|
|
||||||
logger.LogError("Failed to WriteProcessMemory at FpsAddress, error code 0x{Code:X8}", (uint)error);
|
|
||||||
context.Description = SH.FormatServiceGameUnlockerWriteProcessMemoryFpsAddressFailed(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.Report();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.IsUnlockerValid = false;
|
|
||||||
context.FpsAddress = 0;
|
|
||||||
context.Report();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe bool UnsafeWriteProcessMemory(HANDLE hProcess, nuint baseAddress, int value)
|
|
||||||
{
|
|
||||||
return WriteProcessMemory(hProcess, (void*)baseAddress, ref value, out _);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查找模块结果
|
|
||||||
/// </summary>
|
|
||||||
internal enum FindModuleResult
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 成功
|
|
||||||
/// </summary>
|
|
||||||
Ok,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 超时
|
|
||||||
/// </summary>
|
|
||||||
TimeLimitExeeded,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 模块尚未加载
|
|
||||||
/// </summary>
|
|
||||||
ModuleNotLoaded,
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 没有模块,保护驱动已加载,无法读取
|
|
||||||
/// </summary>
|
|
||||||
NoModuleFound,
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Credit to https://github.com/34736384/genshin-fps-unlock
|
|
||||||
/// </summary>
|
|
||||||
internal static class GameFpsAddress
|
|
||||||
{
|
|
||||||
#pragma warning disable SA1310
|
|
||||||
private const byte ASM_CALL = 0xE8;
|
|
||||||
private const byte ASM_JMP = 0xE9;
|
|
||||||
#pragma warning restore SA1310
|
|
||||||
|
|
||||||
public static unsafe void UnsafeFindFpsAddress(GameFpsUnlockerContext context, in RequiredRemoteModule remoteModule, in RequiredLocalModule localModule)
|
|
||||||
{
|
|
||||||
Span<byte> executableSpan = localModule.Executable.AsSpan();
|
|
||||||
int offsetToExecutable = 0;
|
|
||||||
nuint localVirtualAddress = 0;
|
|
||||||
do
|
|
||||||
{
|
|
||||||
int index = IndexOfPattern(executableSpan[offsetToExecutable..], out int patternLength);
|
|
||||||
if (index < 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
offsetToExecutable += index;
|
|
||||||
|
|
||||||
nuint rip = localModule.Executable.Address + (uint)offsetToExecutable;
|
|
||||||
rip += 5U;
|
|
||||||
rip += (nuint)(*(int*)(rip + 1U) + 5);
|
|
||||||
|
|
||||||
if (*(byte*)rip is ASM_JMP)
|
|
||||||
{
|
|
||||||
localVirtualAddress = rip;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
offsetToExecutable += patternLength;
|
|
||||||
}
|
|
||||||
while (true);
|
|
||||||
|
|
||||||
ArgumentOutOfRangeException.ThrowIfZero(localVirtualAddress);
|
|
||||||
|
|
||||||
while (*(byte*)localVirtualAddress is ASM_CALL or ASM_JMP)
|
|
||||||
{
|
|
||||||
localVirtualAddress += (nuint)(*(int*)(localVirtualAddress + 1) + 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
localVirtualAddress += *(uint*)(localVirtualAddress + 2) + 6;
|
|
||||||
nuint relativeVirtualAddress = localVirtualAddress - localModule.Executable.Address;
|
|
||||||
context.FpsAddress = remoteModule.Executable.Address + relativeVirtualAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int IndexOfPattern(in ReadOnlySpan<byte> span, out int patternLength)
|
|
||||||
{
|
|
||||||
// B9 3C 00 00 00 E8
|
|
||||||
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xE8];
|
|
||||||
patternLength = part.Length;
|
|
||||||
return span.IndexOf(part);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,43 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
|
using Snap.Hutao.Service.Feature;
|
||||||
|
using Snap.Hutao.Service.Game.Unlocker.Island;
|
||||||
using Snap.Hutao.Win32.Foundation;
|
using Snap.Hutao.Win32.Foundation;
|
||||||
using Snap.Hutao.Win32.System.LibraryLoader;
|
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||||
using Snap.Hutao.Win32.System.Threading;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.IO.MemoryMappedFiles;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using static Snap.Hutao.Win32.ConstValues;
|
||||||
using static Snap.Hutao.Win32.Kernel32;
|
using static Snap.Hutao.Win32.Kernel32;
|
||||||
|
using static Snap.Hutao.Win32.Macros;
|
||||||
|
using static Snap.Hutao.Win32.User32;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||||
|
|
||||||
internal abstract class GameFpsUnlocker : IGameFpsUnlocker
|
internal sealed class GameFpsUnlocker : IGameFpsUnlocker
|
||||||
{
|
{
|
||||||
|
private const string IslandEnvironmentName = "4F3E8543-40F7-4808-82DC-21E48A6037A7";
|
||||||
private readonly LaunchOptions launchOptions;
|
private readonly LaunchOptions launchOptions;
|
||||||
private readonly GameFpsUnlockerContext context = new();
|
private readonly IFeatureService featureService;
|
||||||
|
|
||||||
public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, in UnlockOptions options, IProgress<GameFpsUnlockerContext> progress)
|
private readonly GameFpsUnlockerContext context = new();
|
||||||
|
private readonly string dataFolderIslandPath;
|
||||||
|
|
||||||
|
private IslandFunctionOffsets? offsets;
|
||||||
|
|
||||||
|
public GameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, IProgress<GameFpsUnlockerContext> progress)
|
||||||
{
|
{
|
||||||
launchOptions = serviceProvider.GetRequiredService<LaunchOptions>();
|
launchOptions = serviceProvider.GetRequiredService<LaunchOptions>();
|
||||||
|
featureService = serviceProvider.GetRequiredService<IFeatureService>();
|
||||||
|
|
||||||
|
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||||
|
dataFolderIslandPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.UnlockerIsland.dll");
|
||||||
|
|
||||||
context.GameProcess = gameProcess;
|
context.GameProcess = gameProcess;
|
||||||
context.AllAccess = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_ALL_ACCESS, false, (uint)gameProcess.Id);
|
|
||||||
context.Options = options;
|
|
||||||
context.Progress = progress;
|
context.Progress = progress;
|
||||||
context.Logger = serviceProvider.GetRequiredService<ILogger<GameFpsUnlocker>>();
|
context.Logger = serviceProvider.GetRequiredService<ILogger<GameFpsUnlocker>>();
|
||||||
}
|
}
|
||||||
@@ -29,31 +45,114 @@ internal abstract class GameFpsUnlocker : IGameFpsUnlocker
|
|||||||
public async ValueTask<bool> UnlockAsync(CancellationToken token = default)
|
public async ValueTask<bool> UnlockAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
HutaoException.ThrowIfNot(context.IsUnlockerValid, "This Unlocker is invalid");
|
HutaoException.ThrowIfNot(context.IsUnlockerValid, "This Unlocker is invalid");
|
||||||
(FindModuleResult result, RequiredRemoteModule remoteModule) = await GameProcessModule.FindModuleAsync(context).ConfigureAwait(false);
|
if (await featureService.GetIslandFeatureAsync().ConfigureAwait(false) is not { } feature)
|
||||||
HutaoException.ThrowIfNot(result != FindModuleResult.TimeLimitExeeded, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
|
|
||||||
HutaoException.ThrowIfNot(result != FindModuleResult.NoModuleFound, SH.ServiceGameUnlockerFindModuleNoModuleFound);
|
|
||||||
|
|
||||||
using (RequiredLocalModule localModule = LoadRequiredLocalModule(context.Options.GameFileSystem))
|
|
||||||
{
|
{
|
||||||
GameFpsAddress.UnsafeFindFpsAddress(context, remoteModule, localModule);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
offsets = string.Equals(GameConstants.GenshinImpactProcessName, context.GameProcess.ProcessName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
? feature.Oversea
|
||||||
|
: feature.Chinese;
|
||||||
|
|
||||||
context.Report();
|
context.Report();
|
||||||
return context.FpsAddress != 0U;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask PostUnlockAsync(CancellationToken token = default)
|
public async ValueTask PostUnlockAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return PostUnlockOverrideAsync(context, launchOptions, context.Logger, token);
|
if (offsets is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(InstalledLocation.GetAbsolutePath("Snap.Hutao.UnlockerIsland.dll"), dataFolderIslandPath, true);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
context.Logger.LogError("Failed to copy island file.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using (MemoryMappedFile file = MemoryMappedFile.CreateOrOpen(IslandEnvironmentName, 1024))
|
||||||
|
{
|
||||||
|
using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor())
|
||||||
|
{
|
||||||
|
nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle();
|
||||||
|
InitializeIslandEnvironment(handle, offsets, launchOptions);
|
||||||
|
InitializeIsland(context.GameProcess);
|
||||||
|
using (PeriodicTimer timer = new(TimeSpan.FromMilliseconds(500)))
|
||||||
|
{
|
||||||
|
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
if (context.GameProcess.HasExited)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
IslandEnvironmentView view = UpdateIslandEnvironment(handle, launchOptions);
|
||||||
|
context.Logger.LogDebug("Island Environment|{State}|{Error}", view.State, view.LastError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
context.Logger.LogInformation("Exit PostUnlockAsync");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract ValueTask PostUnlockOverrideAsync(GameFpsUnlockerContext context, LaunchOptions launchOptions, ILogger logger, CancellationToken token = default);
|
private static unsafe void InitializeIslandEnvironment(nint handle, IslandFunctionOffsets offsets, LaunchOptions options)
|
||||||
|
|
||||||
private static RequiredLocalModule LoadRequiredLocalModule(GameFileSystem gameFileSystem)
|
|
||||||
{
|
{
|
||||||
LOAD_LIBRARY_FLAGS flags = LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_IMAGE_RESOURCE;
|
IslandEnvironment* pIslandEnvironment = (IslandEnvironment*)handle;
|
||||||
HMODULE executaleAddress = LoadLibraryExW(gameFileSystem.GameFilePath, default, flags);
|
pIslandEnvironment->FunctionOffsetFieldOfView = offsets.FunctionOffsetFieldOfView;
|
||||||
|
pIslandEnvironment->FunctionOffsetTargetFrameRate = offsets.FunctionOffsetTargetFrameRate;
|
||||||
|
pIslandEnvironment->FunctionOffsetFog = offsets.FunctionOffsetFog;
|
||||||
|
|
||||||
return new(executaleAddress);
|
UpdateIslandEnvironment(handle, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe IslandEnvironmentView UpdateIslandEnvironment(nint handle, LaunchOptions options)
|
||||||
|
{
|
||||||
|
IslandEnvironment* pIslandEnvironment = (IslandEnvironment*)handle;
|
||||||
|
pIslandEnvironment->FieldOfView = 55; // options.TargetFov;
|
||||||
|
pIslandEnvironment->TargetFrameRate = -1; // options.TargetFps;
|
||||||
|
pIslandEnvironment->DisableFog = true; // options.DisableFog;
|
||||||
|
|
||||||
|
return *(IslandEnvironmentView*)pIslandEnvironment;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void InitializeIsland(Process gameProcess)
|
||||||
|
{
|
||||||
|
HANDLE hModule = default;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
hModule = NativeLibrary.Load(dataFolderIslandPath);
|
||||||
|
nint pIslandGetWindowHook = NativeLibrary.GetExport((nint)(hModule & ~0x3L), "IslandGetWindowHook");
|
||||||
|
|
||||||
|
HOOKPROC hookProc = default;
|
||||||
|
((delegate* unmanaged[Stdcall]<HOOKPROC*, HRESULT>)pIslandGetWindowHook)(&hookProc);
|
||||||
|
|
||||||
|
SpinWait.SpinUntil(() => gameProcess.MainWindowHandle is not 0);
|
||||||
|
uint threadId = GetWindowThreadProcessId(gameProcess.MainWindowHandle, default);
|
||||||
|
HHOOK hHook = SetWindowsHookExW(WINDOWS_HOOK_ID.WH_GETMESSAGE, hookProc, (HINSTANCE)hModule, threadId);
|
||||||
|
if (hHook.Value is 0)
|
||||||
|
{
|
||||||
|
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PostThreadMessageW(threadId, WM_NULL, default, default))
|
||||||
|
{
|
||||||
|
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
NativeLibrary.Free(hModule);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Win32.Foundation;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||||
@@ -10,18 +9,10 @@ internal sealed class GameFpsUnlockerContext
|
|||||||
{
|
{
|
||||||
public string Description { get; set; } = default!;
|
public string Description { get; set; } = default!;
|
||||||
|
|
||||||
public FindModuleResult FindModuleResult { get; set; }
|
|
||||||
|
|
||||||
public bool IsUnlockerValid { get; set; } = true;
|
public bool IsUnlockerValid { get; set; } = true;
|
||||||
|
|
||||||
public nuint FpsAddress { get; set; }
|
|
||||||
|
|
||||||
public UnlockOptions Options { get; set; }
|
|
||||||
|
|
||||||
public Process GameProcess { get; set; } = default!;
|
public Process GameProcess { get; set; } = default!;
|
||||||
|
|
||||||
public HANDLE AllAccess { get; set; }
|
|
||||||
|
|
||||||
public IProgress<GameFpsUnlockerContext> Progress { get; set; } = default!;
|
public IProgress<GameFpsUnlockerContext> Progress { get; set; } = default!;
|
||||||
|
|
||||||
public ILogger Logger { get; set; } = default!;
|
public ILogger Logger { get; set; } = default!;
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
internal enum GameFpsUnlockerKind
|
|
||||||
{
|
|
||||||
Legacy,
|
|
||||||
Island,
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Core.Diagnostics;
|
|
||||||
using Snap.Hutao.Win32.Foundation;
|
|
||||||
using Snap.Hutao.Win32.Memory;
|
|
||||||
using Snap.Hutao.Win32.System.ProcessStatus;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using static Snap.Hutao.Win32.Kernel32;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
internal static class GameProcessModule
|
|
||||||
{
|
|
||||||
public static async ValueTask<ValueResult<FindModuleResult, RequiredRemoteModule>> FindModuleAsync(GameFpsUnlockerContext state)
|
|
||||||
{
|
|
||||||
ValueStopwatch watch = ValueStopwatch.StartNew();
|
|
||||||
using (PeriodicTimer timer = new(state.Options.FindModuleDelay))
|
|
||||||
{
|
|
||||||
while (await timer.WaitForNextTickAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
FindModuleResult result = UnsafeGetGameModuleInfo(state.AllAccess, out RequiredRemoteModule gameModule);
|
|
||||||
if (result == FindModuleResult.Ok)
|
|
||||||
{
|
|
||||||
return new(FindModuleResult.Ok, gameModule);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result == FindModuleResult.NoModuleFound)
|
|
||||||
{
|
|
||||||
return new(FindModuleResult.NoModuleFound, default);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (watch.GetElapsedTime() > state.Options.FindModuleLimit)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new(FindModuleResult.TimeLimitExeeded, default);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static FindModuleResult UnsafeGetGameModuleInfo(in HANDLE hProcess, out RequiredRemoteModule info)
|
|
||||||
{
|
|
||||||
FindModuleResult result = UnsafeFindModule(hProcess, GameConstants.YuanShenFileName, GameConstants.GenshinImpactFileName, out Module executable);
|
|
||||||
|
|
||||||
if (result is FindModuleResult.Ok)
|
|
||||||
{
|
|
||||||
info = new(executable);
|
|
||||||
return FindModuleResult.Ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result is FindModuleResult.NoModuleFound)
|
|
||||||
{
|
|
||||||
info = default;
|
|
||||||
return FindModuleResult.NoModuleFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
info = default;
|
|
||||||
return FindModuleResult.ModuleNotLoaded;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe FindModuleResult UnsafeFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName1, in ReadOnlySpan<char> moduleName2, out Module module)
|
|
||||||
{
|
|
||||||
HMODULE[] buffer = new HMODULE[128];
|
|
||||||
if (!K32EnumProcessModules(hProcess, buffer, out uint actualSize))
|
|
||||||
{
|
|
||||||
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actualSize == 0)
|
|
||||||
{
|
|
||||||
module = default!;
|
|
||||||
return FindModuleResult.NoModuleFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (ref readonly HMODULE hModule in buffer.AsSpan()[..(int)(actualSize / sizeof(HMODULE))])
|
|
||||||
{
|
|
||||||
char[] baseName = new char[256];
|
|
||||||
|
|
||||||
if (K32GetModuleBaseNameW(hProcess, hModule, baseName) == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fixed (char* lpBaseName = baseName)
|
|
||||||
{
|
|
||||||
ReadOnlySpan<char> baseNameSpan = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName);
|
|
||||||
if (!(moduleName1.SequenceEqual(baseNameSpan) || moduleName2.SequenceEqual(baseNameSpan)))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!K32GetModuleInformation(hProcess, hModule, out MODULEINFO moduleInfo))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
module = new((nuint)moduleInfo.lpBaseOfDll, moduleInfo.SizeOfImage);
|
|
||||||
return FindModuleResult.Ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
module = default;
|
|
||||||
return FindModuleResult.ModuleNotLoaded;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,9 +7,15 @@ namespace Snap.Hutao.Service.Game.Unlocker.Island;
|
|||||||
|
|
||||||
internal struct IslandEnvironment
|
internal struct IslandEnvironment
|
||||||
{
|
{
|
||||||
public nuint Address;
|
public nuint Reserved1;
|
||||||
public int Value;
|
public int Reserved2;
|
||||||
public IslandState State;
|
public IslandState State;
|
||||||
public WIN32_ERROR LastError;
|
public WIN32_ERROR LastError;
|
||||||
public int Reserved;
|
public int Reserved3;
|
||||||
|
public float FieldOfView;
|
||||||
|
public int TargetFrameRate;
|
||||||
|
public bool DisableFog;
|
||||||
|
public nuint FunctionOffsetFieldOfView;
|
||||||
|
public nuint FunctionOffsetTargetFrameRate;
|
||||||
|
public nuint FunctionOffsetFog;
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,15 @@ namespace Snap.Hutao.Service.Game.Unlocker.Island;
|
|||||||
|
|
||||||
internal struct IslandEnvironmentView
|
internal struct IslandEnvironmentView
|
||||||
{
|
{
|
||||||
public nuint Address;
|
public nuint Reserved1;
|
||||||
public int Value;
|
public int Reserved2;
|
||||||
public IslandState State;
|
public IslandState State;
|
||||||
public WIN32_ERROR LastError;
|
public WIN32_ERROR LastError;
|
||||||
public int Reserved;
|
public int Reserved3;
|
||||||
|
public float FieldOfView;
|
||||||
|
public int TargetFrameRate;
|
||||||
|
public bool DisableFog;
|
||||||
|
public nuint FunctionOffsetFieldOfView;
|
||||||
|
public nuint FunctionOffsetTargetFrameRate;
|
||||||
|
public nuint FunctionOffsetFog;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Unlocker.Island;
|
||||||
|
|
||||||
|
internal sealed class IslandFeature
|
||||||
|
{
|
||||||
|
public required IslandFunctionOffsets Oversea { get; set; }
|
||||||
|
|
||||||
|
public required IslandFunctionOffsets Chinese { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Game.Unlocker.Island;
|
||||||
|
|
||||||
|
internal sealed class IslandFunctionOffsets
|
||||||
|
{
|
||||||
|
public required uint FunctionOffsetFieldOfView { get; set; }
|
||||||
|
|
||||||
|
public required uint FunctionOffsetTargetFrameRate { get; set; }
|
||||||
|
|
||||||
|
public required uint FunctionOffsetFog { get; set; }
|
||||||
|
}
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Core;
|
|
||||||
using Snap.Hutao.Win32.Foundation;
|
|
||||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
|
||||||
using System.IO.MemoryMappedFiles;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using Windows.Storage;
|
|
||||||
using static Snap.Hutao.Win32.ConstValues;
|
|
||||||
using static Snap.Hutao.Win32.Kernel32;
|
|
||||||
using static Snap.Hutao.Win32.Macros;
|
|
||||||
using static Snap.Hutao.Win32.User32;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker.Island;
|
|
||||||
|
|
||||||
internal sealed class IslandGameFpsUnlocker : GameFpsUnlocker
|
|
||||||
{
|
|
||||||
private const string IslandEnvironmentName = "4F3E8543-40F7-4808-82DC-21E48A6037A7";
|
|
||||||
|
|
||||||
private readonly string dataFolderIslandPath;
|
|
||||||
|
|
||||||
public IslandGameFpsUnlocker(IServiceProvider serviceProvider, Process gameProcess, in UnlockOptions options, IProgress<GameFpsUnlockerContext> progress)
|
|
||||||
: base(serviceProvider, gameProcess, options, progress)
|
|
||||||
{
|
|
||||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
|
||||||
dataFolderIslandPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.UnlockerIsland.dll");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async ValueTask PostUnlockOverrideAsync(GameFpsUnlockerContext context, LaunchOptions launchOptions, ILogger logger, CancellationToken token = default(CancellationToken))
|
|
||||||
{
|
|
||||||
if (!await InitializeIslandFileAsync().ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
context.Logger.LogError("Failed to copy island file.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (MemoryMappedFile file = MemoryMappedFile.CreateOrOpen(IslandEnvironmentName, 1024))
|
|
||||||
{
|
|
||||||
using (MemoryMappedViewAccessor accessor = file.CreateViewAccessor())
|
|
||||||
{
|
|
||||||
nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle();
|
|
||||||
UpdateIslandEnvironment(handle, context, launchOptions);
|
|
||||||
InitializeIsland(context.GameProcess);
|
|
||||||
|
|
||||||
using (PeriodicTimer timer = new(context.Options.AdjustFpsDelay))
|
|
||||||
{
|
|
||||||
while (await timer.WaitForNextTickAsync(token).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("context.GameProcess.HasExited: {Value}", context.GameProcess.HasExited);
|
|
||||||
if (!context.GameProcess.HasExited && context.FpsAddress != 0U)
|
|
||||||
{
|
|
||||||
IslandEnvironmentView view = UpdateIslandEnvironment(handle, context, launchOptions);
|
|
||||||
context.Logger.LogDebug("Island Environment|{State}|{Error}", view.State, view.LastError);
|
|
||||||
context.Report();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
context.IsUnlockerValid = false;
|
|
||||||
context.FpsAddress = 0;
|
|
||||||
context.Report();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
context.Logger.LogInformation("Exit PostUnlockOverrideAsync");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async ValueTask<bool> InitializeIslandFileAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Uri islandUri = "ms-appx:///Snap.Hutao.UnlockerIsland.dll".ToUri();
|
|
||||||
StorageFile islandFile = await StorageFile.GetFileFromApplicationUriAsync(islandUri);
|
|
||||||
await islandFile.OverwriteCopyAsync(dataFolderIslandPath).ConfigureAwait(false);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void InitializeIsland(Process gameProcess)
|
|
||||||
{
|
|
||||||
HANDLE hModule = default;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
hModule = NativeLibrary.Load(dataFolderIslandPath);
|
|
||||||
nint pIslandGetWindowHook = NativeLibrary.GetExport((nint)(hModule & ~0x3L), "IslandGetWindowHook");
|
|
||||||
|
|
||||||
HOOKPROC hookProc = default;
|
|
||||||
((delegate* unmanaged[Stdcall]<HOOKPROC*, HRESULT>)pIslandGetWindowHook)(&hookProc);
|
|
||||||
|
|
||||||
SpinWait.SpinUntil(() => gameProcess.MainWindowHandle is not 0);
|
|
||||||
uint threadId = GetWindowThreadProcessId(gameProcess.MainWindowHandle, default);
|
|
||||||
HHOOK hHook = SetWindowsHookExW(WINDOWS_HOOK_ID.WH_GETMESSAGE, hookProc, (HINSTANCE)hModule, threadId);
|
|
||||||
if (hHook.Value is 0)
|
|
||||||
{
|
|
||||||
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PostThreadMessageW(threadId, WM_NULL, default, default))
|
|
||||||
{
|
|
||||||
Marshal.ThrowExceptionForHR(HRESULT_FROM_WIN32(GetLastError()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
NativeLibrary.Free(hModule);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe IslandEnvironmentView UpdateIslandEnvironment(nint handle, GameFpsUnlockerContext context, LaunchOptions launchOptions)
|
|
||||||
{
|
|
||||||
IslandEnvironment* pIslandEnvironment = (IslandEnvironment*)handle;
|
|
||||||
pIslandEnvironment->Address = context.FpsAddress;
|
|
||||||
pIslandEnvironment->Value = launchOptions.TargetFps;
|
|
||||||
|
|
||||||
return *(IslandEnvironmentView*)pIslandEnvironment;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
internal readonly struct Module
|
|
||||||
{
|
|
||||||
public readonly bool HasValue = false;
|
|
||||||
public readonly nuint Address;
|
|
||||||
public readonly uint Size;
|
|
||||||
|
|
||||||
public Module(nuint address, uint size)
|
|
||||||
{
|
|
||||||
HasValue = true;
|
|
||||||
Address = address;
|
|
||||||
Size = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
public unsafe Span<byte> AsSpan()
|
|
||||||
{
|
|
||||||
return new((void*)Address, (int)Size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
using Snap.Hutao.Win32.Foundation;
|
|
||||||
using Snap.Hutao.Win32.System.Diagnostics.Debug;
|
|
||||||
using Snap.Hutao.Win32.System.SystemService;
|
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using static Snap.Hutao.Win32.Kernel32;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
internal readonly struct RequiredLocalModule : IDisposable
|
|
||||||
{
|
|
||||||
public readonly bool HasValue = false;
|
|
||||||
public readonly Module Executable;
|
|
||||||
|
|
||||||
private readonly HMODULE hModuleExecutable;
|
|
||||||
|
|
||||||
public RequiredLocalModule(HMODULE executable)
|
|
||||||
{
|
|
||||||
hModuleExecutable = executable;
|
|
||||||
|
|
||||||
// Align the pointer
|
|
||||||
nint executableMappedView = (nint)(executable & ~0x3L);
|
|
||||||
|
|
||||||
Executable = new((nuint)executableMappedView, GetImageSize(executableMappedView));
|
|
||||||
HasValue = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
FreeLibrary(hModuleExecutable);
|
|
||||||
}
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
||||||
private unsafe uint GetImageSize(nint baseAddress)
|
|
||||||
{
|
|
||||||
IMAGE_DOS_HEADER* pImageDosHeader = (IMAGE_DOS_HEADER*)baseAddress;
|
|
||||||
IMAGE_NT_HEADERS64* pImageNtHeader = (IMAGE_NT_HEADERS64*)(pImageDosHeader->e_lfanew + baseAddress);
|
|
||||||
return pImageNtHeader->OptionalHeader.SizeOfImage;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
internal readonly struct RequiredRemoteModule
|
|
||||||
{
|
|
||||||
public readonly bool HasValue = false;
|
|
||||||
public readonly Module Executable;
|
|
||||||
|
|
||||||
public RequiredRemoteModule(in Module executable)
|
|
||||||
{
|
|
||||||
HasValue = true;
|
|
||||||
Executable = executable;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
|
||||||
|
|
||||||
internal readonly struct UnlockOptions
|
|
||||||
{
|
|
||||||
public readonly GameFileSystem GameFileSystem;
|
|
||||||
public readonly TimeSpan FindModuleDelay;
|
|
||||||
public readonly TimeSpan FindModuleLimit;
|
|
||||||
public readonly TimeSpan AdjustFpsDelay;
|
|
||||||
|
|
||||||
public UnlockOptions(GameFileSystem gameFileSystem, int findModuleDelayMilliseconds, int findModuleLimitMilliseconds, int adjustFpsDelayMilliseconds)
|
|
||||||
{
|
|
||||||
GameFileSystem = gameFileSystem;
|
|
||||||
FindModuleDelay = TimeSpan.FromMilliseconds(findModuleDelayMilliseconds);
|
|
||||||
FindModuleLimit = TimeSpan.FromMilliseconds(findModuleLimitMilliseconds);
|
|
||||||
AdjustFpsDelay = TimeSpan.FromMilliseconds(adjustFpsDelayMilliseconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,5 +9,5 @@ internal sealed class CheckUpdateResult
|
|||||||
{
|
{
|
||||||
public CheckUpdateResultKind Kind { get; set; }
|
public CheckUpdateResultKind Kind { get; set; }
|
||||||
|
|
||||||
public HutaoVersionInformation? HutaoVersionInformation { get; set; }
|
public HutaoPackageInformation? PackageInformation { get; set; }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Web.Hutao;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Service.Update;
|
||||||
|
|
||||||
|
internal sealed class HutaoSelectedMirrorInformation
|
||||||
|
{
|
||||||
|
public required Version Version { get; set; }
|
||||||
|
|
||||||
|
public required string Validation { get; set; }
|
||||||
|
|
||||||
|
public required HutaoPackageMirror Mirror { get; set; }
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ internal interface IUpdateService
|
|||||||
{
|
{
|
||||||
ValueTask<CheckUpdateResult> CheckUpdateAsync(IProgress<UpdateStatus> progress, CancellationToken token = default);
|
ValueTask<CheckUpdateResult> CheckUpdateAsync(IProgress<UpdateStatus> progress, CancellationToken token = default);
|
||||||
|
|
||||||
ValueTask<bool> DownloadUpdateAsync(CheckUpdateResult checkUpdateResult, IProgress<UpdateStatus> progress, CancellationToken token = default);
|
ValueTask<bool> DownloadUpdateAsync(HutaoSelectedMirrorInformation mirrorInformation, IProgress<UpdateStatus> progress, CancellationToken token = default);
|
||||||
|
|
||||||
ValueTask<LaunchUpdaterResult> LaunchUpdaterAsync();
|
LaunchUpdaterResult LaunchUpdater();
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Core.IO;
|
||||||
using Snap.Hutao.Core.IO.Hashing;
|
using Snap.Hutao.Core.IO.Hashing;
|
||||||
using Snap.Hutao.Core.IO.Http.Sharding;
|
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Service.Abstraction;
|
using Snap.Hutao.Service.Abstraction;
|
||||||
using Snap.Hutao.Service.Notification;
|
using Snap.Hutao.Service.Notification;
|
||||||
@@ -11,8 +11,8 @@ using Snap.Hutao.Web.Hutao;
|
|||||||
using Snap.Hutao.Web.Hutao.Response;
|
using Snap.Hutao.Web.Hutao.Response;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using Windows.Storage;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Update;
|
namespace Snap.Hutao.Service.Update;
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ internal sealed partial class UpdateService : IUpdateService
|
|||||||
await taskContext.SwitchToBackgroundAsync();
|
await taskContext.SwitchToBackgroundAsync();
|
||||||
|
|
||||||
HutaoInfrastructureClient infrastructureClient = scope.ServiceProvider.GetRequiredService<HutaoInfrastructureClient>();
|
HutaoInfrastructureClient infrastructureClient = scope.ServiceProvider.GetRequiredService<HutaoInfrastructureClient>();
|
||||||
HutaoResponse<HutaoVersionInformation> response = await infrastructureClient.GetHutaoVersionInfomationAsync(token).ConfigureAwait(false);
|
HutaoResponse<HutaoPackageInformation> response = await infrastructureClient.GetHutaoVersionInfomationAsync(token).ConfigureAwait(false);
|
||||||
|
|
||||||
CheckUpdateResult checkUpdateResult = new();
|
CheckUpdateResult checkUpdateResult = new();
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ internal sealed partial class UpdateService : IUpdateService
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
checkUpdateResult.Kind = CheckUpdateResultKind.NeedDownload;
|
checkUpdateResult.Kind = CheckUpdateResultKind.NeedDownload;
|
||||||
checkUpdateResult.HutaoVersionInformation = response.Data;
|
checkUpdateResult.PackageInformation = response.Data;
|
||||||
}
|
}
|
||||||
|
|
||||||
string msixPath = GetUpdatePackagePath();
|
string msixPath = GetUpdatePackagePath();
|
||||||
@@ -52,7 +52,7 @@ internal sealed partial class UpdateService : IUpdateService
|
|||||||
if (!LocalSetting.Get(SettingKeys.OverrideUpdateVersionComparison, false))
|
if (!LocalSetting.Get(SettingKeys.OverrideUpdateVersionComparison, false))
|
||||||
{
|
{
|
||||||
// Launched in an updated version
|
// Launched in an updated version
|
||||||
if (scope.ServiceProvider.GetRequiredService<RuntimeOptions>().Version >= checkUpdateResult.HutaoVersionInformation.Version)
|
if (scope.ServiceProvider.GetRequiredService<RuntimeOptions>().Version >= checkUpdateResult.PackageInformation.Version)
|
||||||
{
|
{
|
||||||
if (File.Exists(msixPath))
|
if (File.Exists(msixPath))
|
||||||
{
|
{
|
||||||
@@ -64,9 +64,9 @@ internal sealed partial class UpdateService : IUpdateService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.Report(new(checkUpdateResult.HutaoVersionInformation.Version.ToString(), 0, 0));
|
progress.Report(new(checkUpdateResult.PackageInformation.Version.ToString(), 0, 0));
|
||||||
|
|
||||||
if (checkUpdateResult.HutaoVersionInformation.Sha256 is not { Length: > 0 } sha256)
|
if (checkUpdateResult.PackageInformation.Validation is not { Length: > 0 } sha256)
|
||||||
{
|
{
|
||||||
checkUpdateResult.Kind = CheckUpdateResultKind.VersionApiInvalidSha256;
|
checkUpdateResult.Kind = CheckUpdateResultKind.VersionApiInvalidSha256;
|
||||||
return checkUpdateResult;
|
return checkUpdateResult;
|
||||||
@@ -82,20 +82,17 @@ internal sealed partial class UpdateService : IUpdateService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<bool> DownloadUpdateAsync(CheckUpdateResult checkUpdateResult, IProgress<UpdateStatus> progress, CancellationToken token = default)
|
public ValueTask<bool> DownloadUpdateAsync(HutaoSelectedMirrorInformation mirrorInformation, IProgress<UpdateStatus> progress, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(checkUpdateResult.HutaoVersionInformation);
|
return DownloadUpdatePackageAsync(mirrorInformation, GetUpdatePackagePath(), progress, token);
|
||||||
return DownloadUpdatePackageAsync(checkUpdateResult.HutaoVersionInformation, GetUpdatePackagePath(), progress, token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<LaunchUpdaterResult> LaunchUpdaterAsync()
|
public LaunchUpdaterResult LaunchUpdater()
|
||||||
{
|
{
|
||||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||||
string updaterTargetPath = runtimeOptions.GetDataFolderUpdateCacheFolderFile(UpdaterFilename);
|
string updaterTargetPath = runtimeOptions.GetDataFolderUpdateCacheFolderFile(UpdaterFilename);
|
||||||
|
|
||||||
Uri updaterSourceUri = $"ms-appx:///{UpdaterFilename}".ToUri();
|
File.Copy(InstalledLocation.GetAbsolutePath(UpdaterFilename), updaterTargetPath, true);
|
||||||
StorageFile updaterFile = await StorageFile.GetFileFromApplicationUriAsync(updaterSourceUri);
|
|
||||||
await updaterFile.OverwriteCopyAsync(updaterTargetPath).ConfigureAwait(false);
|
|
||||||
|
|
||||||
string commandLine = new CommandLineBuilder()
|
string commandLine = new CommandLineBuilder()
|
||||||
.Append("--package-path", GetUpdatePackagePath(runtimeOptions))
|
.Append("--package-path", GetUpdatePackagePath(runtimeOptions))
|
||||||
@@ -133,37 +130,68 @@ internal sealed partial class UpdateService : IUpdateService
|
|||||||
return runtimeOptions.GetDataFolderUpdateCacheFolderFile("Snap.Hutao.msix");
|
return runtimeOptions.GetDataFolderUpdateCacheFolderFile("Snap.Hutao.msix");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<bool> DownloadUpdatePackageAsync(HutaoVersionInformation versionInformation, string filePath, IProgress<UpdateStatus> progress, CancellationToken token = default)
|
private async ValueTask<bool> DownloadUpdatePackageAsync(HutaoSelectedMirrorInformation mirrorInformation, string filePath, IProgress<UpdateStatus> progress, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
using (IServiceScope scope = serviceProvider.CreateScope())
|
using IServiceScope scope = serviceProvider.CreateScope();
|
||||||
|
using HttpClient httpClient = scope.ServiceProvider.GetRequiredService<HttpClient>();
|
||||||
|
|
||||||
|
HutaoPackageMirror mirror = mirrorInformation.Mirror;
|
||||||
|
string version = mirrorInformation.Version.ToString();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
using (HttpClient httpClient = scope.ServiceProvider.GetRequiredService<HttpClient>())
|
using HttpResponseMessage responseMessage = await httpClient.GetAsync(mirror.Url, HttpCompletionOption.ResponseHeadersRead, token).ConfigureAwait(false);
|
||||||
|
long totalBytes = responseMessage.Content.Headers.ContentLength ?? 0;
|
||||||
|
using Stream webStream = await responseMessage.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
switch (mirror.MirrorType)
|
||||||
{
|
{
|
||||||
string version = versionInformation.Version.ToString();
|
case HutaoPackageMirrorType.Direct:
|
||||||
foreach (string url in versionInformation.Urls)
|
using (FileStream fileStream = File.Create(filePath))
|
||||||
{
|
|
||||||
HttpShardCopyWorkerOptions<UpdateStatus> options = new()
|
|
||||||
{
|
{
|
||||||
HttpClient = httpClient,
|
StreamCopyWorker<UpdateStatus> worker = new(webStream, fileStream, bytesRead => new UpdateStatus(version, bytesRead, totalBytes));
|
||||||
SourceUrl = url,
|
await worker.CopyAsync(progress).ConfigureAwait(false);
|
||||||
DestinationFilePath = filePath,
|
|
||||||
MaxDegreeOfParallelism = Math.Clamp(Environment.ProcessorCount, 2, 6),
|
|
||||||
StatusFactory = (bytesRead, totalBytes) => new UpdateStatus(version, bytesRead, totalBytes),
|
|
||||||
};
|
|
||||||
|
|
||||||
using (HttpShardCopyWorker<UpdateStatus> worker = await HttpShardCopyWorker<UpdateStatus>.CreateAsync(options).ConfigureAwait(false))
|
|
||||||
{
|
|
||||||
await worker.CopyAsync(progress, token).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
string? remoteHash = versionInformation.Sha256;
|
break;
|
||||||
ArgumentNullException.ThrowIfNull(remoteHash);
|
case HutaoPackageMirrorType.Archive:
|
||||||
if (await CheckUpdateCacheSHA256Async(filePath, remoteHash, token).ConfigureAwait(false))
|
using (TempFileStream tempFileStream = new(FileMode.Create, FileAccess.ReadWrite))
|
||||||
{
|
{
|
||||||
return true;
|
StreamCopyWorker<UpdateStatus> worker = new(webStream, tempFileStream, bytesRead => new UpdateStatus(version, bytesRead, totalBytes));
|
||||||
|
await worker.CopyAsync(progress).ConfigureAwait(false);
|
||||||
|
|
||||||
|
using ZipArchive archive = new(tempFileStream);
|
||||||
|
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||||
|
{
|
||||||
|
if (!entry.FullName.EndsWith(".msix", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
using Stream entryStream = entry.Open();
|
||||||
|
using FileStream fileStream = File.Create(filePath);
|
||||||
|
await entryStream.CopyToAsync(fileStream, token).ConfigureAwait(false);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? remoteHash = mirrorInformation.Validation;
|
||||||
|
ArgumentNullException.ThrowIfNull(remoteHash);
|
||||||
|
if (await CheckUpdateCacheSHA256Async(filePath, remoteHash, token).ConfigureAwait(false))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -320,7 +320,7 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Snap.Hutao.UnlockerIsland" Version="1.0.10">
|
<PackageReference Include="Snap.Hutao.UnlockerIsland" Version="1.1.4">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ using Snap.Hutao.Core.ExceptionService;
|
|||||||
using Snap.Hutao.Core.Graphics;
|
using Snap.Hutao.Core.Graphics;
|
||||||
using Snap.Hutao.Win32.Foundation;
|
using Snap.Hutao.Win32.Foundation;
|
||||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||||
using System.IO;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -28,8 +27,8 @@ internal sealed class NotifyIconController : IDisposable
|
|||||||
{
|
{
|
||||||
lazyMenu = new(() => new(serviceProvider));
|
lazyMenu = new(() => new(serviceProvider));
|
||||||
|
|
||||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
string iconPath = InstalledLocation.GetAbsolutePath("Assets/Logo.ico");
|
||||||
string iconPath = Path.Combine(runtimeOptions.InstalledLocation, "Assets/Logo.ico");
|
|
||||||
icon = new(iconPath);
|
icon = new(iconPath);
|
||||||
id = Unsafe.As<byte, Guid>(ref MemoryMarshal.GetArrayDataReference(MD5.HashData(Encoding.UTF8.GetBytes(iconPath))));
|
id = Unsafe.As<byte, Guid>(ref MemoryMarshal.GetArrayDataReference(MD5.HashData(Encoding.UTF8.GetBytes(iconPath))));
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ internal sealed class NotifyIconXamlHostWindow : Window, IWindowNeedEraseBackgro
|
|||||||
{
|
{
|
||||||
Content = new Border();
|
Content = new Border();
|
||||||
|
|
||||||
this.SetLayered();
|
this.SetExStyleLayered();
|
||||||
this.SetToolWindow();
|
this.SetExStyleToolWindow();
|
||||||
|
|
||||||
AppWindow.Title = "SnapHutaoNotifyIconXamlHost";
|
AppWindow.Title = "SnapHutaoNotifyIconXamlHost";
|
||||||
AppWindow.IsShownInSwitchers = false;
|
AppWindow.IsShownInSwitchers = false;
|
||||||
@@ -54,7 +54,7 @@ internal sealed class NotifyIconXamlHostWindow : Window, IWindowNeedEraseBackgro
|
|||||||
flyout.ShowAt(Content, new()
|
flyout.ShowAt(Content, new()
|
||||||
{
|
{
|
||||||
Placement = FlyoutPlacementMode.Auto,
|
Placement = FlyoutPlacementMode.Auto,
|
||||||
ShowMode = FlyoutShowMode.Transient,
|
ShowMode = FlyoutShowMode.Standard,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ using Snap.Hutao.UI.Xaml.Media.Backdrop;
|
|||||||
using Snap.Hutao.Win32.Foundation;
|
using Snap.Hutao.Win32.Foundation;
|
||||||
using Snap.Hutao.Win32.Graphics.Dwm;
|
using Snap.Hutao.Win32.Graphics.Dwm;
|
||||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||||
using System.IO;
|
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Windows.Graphics;
|
using Windows.Graphics;
|
||||||
using Windows.UI;
|
using Windows.UI;
|
||||||
@@ -58,7 +57,7 @@ internal sealed class XamlWindowController
|
|||||||
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
||||||
|
|
||||||
window.AppWindow.Title = SH.FormatAppNameAndVersion(runtimeOptions.Version);
|
window.AppWindow.Title = SH.FormatAppNameAndVersion(runtimeOptions.Version);
|
||||||
window.AppWindow.SetIcon(Path.Combine(runtimeOptions.InstalledLocation, "Assets/Logo.ico"));
|
window.AppWindow.SetIcon(InstalledLocation.GetAbsolutePath("Assets/Logo.ico"));
|
||||||
|
|
||||||
// ExtendContentIntoTitleBar
|
// ExtendContentIntoTitleBar
|
||||||
if (window is IXamlWindowExtendContentIntoTitleBar xamlWindow)
|
if (window is IXamlWindowExtendContentIntoTitleBar xamlWindow)
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Microsoft.UI.Xaml.Data;
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using Windows.Foundation;
|
using Windows.Foundation;
|
||||||
using Windows.Foundation.Collections;
|
using Windows.Foundation.Collections;
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ internal sealed class Int32ToVisibilityConverter : IValueConverter
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public object Convert(object value, Type targetType, object parameter, string language)
|
public object Convert(object value, Type targetType, object parameter, string language)
|
||||||
{
|
{
|
||||||
return value is not 0 ? Visibility.Visible : Visibility.Collapsed;
|
return value is not null && value is not 0 ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|||||||
@@ -2,17 +2,21 @@
|
|||||||
x:Class="Snap.Hutao.UI.Xaml.View.Dialog.UpdatePackageDownloadConfirmDialog"
|
x:Class="Snap.Hutao.UI.Xaml.View.Dialog.UpdatePackageDownloadConfirmDialog"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:cwb="using:CommunityToolkit.WinUI.Behaviors"
|
||||||
|
xmlns:cwc="using:CommunityToolkit.WinUI.Controls"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
|
||||||
|
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
|
||||||
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup"
|
xmlns:shuxm="using:Snap.Hutao.UI.Xaml.Markup"
|
||||||
CloseButtonText="{shuxm:ResourceString Name=ContentDialogCancelCloseButtonText}"
|
CloseButtonText="{shuxm:ResourceString Name=ContentDialogCancelCloseButtonText}"
|
||||||
DefaultButton="Primary"
|
DefaultButton="Primary"
|
||||||
PrimaryButtonText="{shuxm:ResourceString Name=ContentDialogConfirmPrimaryButtonText}"
|
PrimaryButtonText="{shuxm:ResourceString Name=ViewDialogUpdatePackagePrimaryText}"
|
||||||
Style="{ThemeResource DefaultContentDialogStyle}"
|
Style="{ThemeResource DefaultContentDialogStyle}"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<StackPanel Spacing="16">
|
<StackPanel Spacing="16">
|
||||||
<TextBlock Text="">
|
<TextBlock>
|
||||||
<Run Text="{shuxm:ResourceString Name=ViewTitileUpdatePackageDownloadContent}"/>
|
<Run Text="{shuxm:ResourceString Name=ViewTitileUpdatePackageDownloadContent}"/>
|
||||||
<Hyperlink NavigateUri="https://hut.ao/statements/update-log.html">
|
<Hyperlink NavigateUri="https://hut.ao/statements/update-log.html">
|
||||||
<Run Text="{shuxm:ResourceString Name=ViewDialogUpdatePackageDownloadUpdatelogLinkContent}"/>
|
<Run Text="{shuxm:ResourceString Name=ViewDialogUpdatePackageDownloadUpdatelogLinkContent}"/>
|
||||||
@@ -20,5 +24,16 @@
|
|||||||
<!-- We leave a Run here to prevent the Hyperlink Stretch -->
|
<!-- We leave a Run here to prevent the Hyperlink Stretch -->
|
||||||
<Run/>
|
<Run/>
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
Header="{shuxm:ResourceString Name=ViewDialogUpdatePackageMirrorHeader}"
|
||||||
|
ItemsSource="{x:Bind Mirrors}"
|
||||||
|
SelectedItem="{x:Bind SelectedItem}">
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<TextBlock Text="{Binding MirrorName}"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ContentDialog>
|
</ContentDialog>
|
||||||
@@ -2,13 +2,26 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Snap.Hutao.Web.Hutao;
|
||||||
|
|
||||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
|
|
||||||
|
[DependencyProperty("Mirrors", typeof(List<HutaoPackageMirror>))]
|
||||||
|
[DependencyProperty("SelectedItem", typeof(HutaoPackageMirror))]
|
||||||
internal sealed partial class UpdatePackageDownloadConfirmDialog : ContentDialog
|
internal sealed partial class UpdatePackageDownloadConfirmDialog : ContentDialog
|
||||||
{
|
{
|
||||||
public UpdatePackageDownloadConfirmDialog()
|
public UpdatePackageDownloadConfirmDialog()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async ValueTask<ValueResult<bool, HutaoPackageMirror?>> GetSelectedMirrorAsync()
|
||||||
|
{
|
||||||
|
if (await ShowAsync() is ContentDialogResult.Primary)
|
||||||
|
{
|
||||||
|
return new(true, SelectedItem ?? Mirrors?.FirstOrDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(false, default);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
x:Name="InfoBarPanelTransitionHelper"
|
x:Name="InfoBarPanelTransitionHelper"
|
||||||
DefaultEasingMode="EaseOut"
|
DefaultEasingMode="EaseOut"
|
||||||
DefaultEasingType="Cubic"
|
DefaultEasingType="Cubic"
|
||||||
Duration="0:0:0.3">
|
Duration="0:0:0.2">
|
||||||
<clw:TransitionConfig Id="Body" ScaleMode="ScaleX"/>
|
<clw:TransitionConfig Id="Body" ScaleMode="ScaleX"/>
|
||||||
<clw:TransitionConfig Id="Header" ScaleMode="ScaleX"/>
|
<clw:TransitionConfig Id="Header" ScaleMode="ScaleX"/>
|
||||||
</clw:TransitionHelper>
|
</clw:TransitionHelper>
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
x:Name="ShowButtonBorder"
|
x:Name="ShowButtonBorder"
|
||||||
Margin="16"
|
Margin="16"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Bottom"
|
||||||
clw:TransitionHelper.Id="Body"
|
clw:TransitionHelper.Id="Body"
|
||||||
Background="{ThemeResource SystemControlAcrylicElementBrush}"
|
Background="{ThemeResource SystemControlAcrylicElementBrush}"
|
||||||
CornerRadius="{ThemeResource ControlCornerRadius}">
|
CornerRadius="{ThemeResource ControlCornerRadius}">
|
||||||
@@ -104,17 +104,28 @@
|
|||||||
x:Name="InfoBarItemsBorder"
|
x:Name="InfoBarItemsBorder"
|
||||||
Margin="16"
|
Margin="16"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Bottom"
|
||||||
clw:TransitionHelper.Id="Body"
|
clw:TransitionHelper.Id="Body"
|
||||||
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
|
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
|
||||||
Visibility="Collapsed">
|
Visibility="Collapsed">
|
||||||
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
|
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
|
||||||
<Grid RowSpacing="16">
|
<Grid RowSpacing="16">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="auto"/>
|
|
||||||
<RowDefinition/>
|
<RowDefinition/>
|
||||||
|
<RowDefinition Height="auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<Grid ColumnSpacing="8">
|
|
||||||
|
<ScrollView Grid.Row="0">
|
||||||
|
<ItemsControl
|
||||||
|
MaxWidth="480"
|
||||||
|
ItemContainerTransitions="{StaticResource AddDeleteThemeTransitions}"
|
||||||
|
ItemTemplateSelector="{StaticResource InfoBarTemplateSelector}"
|
||||||
|
ItemsPanel="{StaticResource StackPanelSpacing8Template}"
|
||||||
|
ItemsSource="{Binding InfoBars, Mode=OneWay}"/>
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
<Grid Grid.Row="1" ColumnSpacing="8">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition/>
|
<ColumnDefinition/>
|
||||||
<ColumnDefinition Width="auto"/>
|
<ColumnDefinition Width="auto"/>
|
||||||
@@ -123,7 +134,7 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
clw:TransitionHelper.Id="Header"
|
clw:TransitionHelper.Id="Header"
|
||||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||||
Text="所有通知"/>
|
Text="{shuxm:ResourceString Name=ViewInfoBarPanelTitle}"/>
|
||||||
<StackPanel
|
<StackPanel
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Orientation="Horizontal"
|
Orientation="Horizontal"
|
||||||
@@ -164,15 +175,6 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ScrollView Grid.Row="1">
|
|
||||||
<ItemsControl
|
|
||||||
MaxWidth="480"
|
|
||||||
ItemContainerTransitions="{StaticResource AddDeleteThemeTransitions}"
|
|
||||||
ItemTemplateSelector="{StaticResource InfoBarTemplateSelector}"
|
|
||||||
ItemsPanel="{StaticResource StackPanelSpacing8Template}"
|
|
||||||
ItemsSource="{Binding InfoBars, Mode=OneWay}"/>
|
|
||||||
</ScrollView>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -64,7 +64,9 @@
|
|||||||
Source="{Binding Icon}"/>
|
Source="{Binding Icon}"/>
|
||||||
</shuxcc:VerticalCard.Top>
|
</shuxcc:VerticalCard.Top>
|
||||||
<shuxcc:VerticalCard.Bottom>
|
<shuxcc:VerticalCard.Bottom>
|
||||||
<TextBlock Text="{Binding Level}"/>
|
<Viewbox MaxWidth="52" StretchDirection="DownOnly">
|
||||||
|
<TextBlock Text="{Binding Level}"/>
|
||||||
|
</Viewbox>
|
||||||
</shuxcc:VerticalCard.Bottom>
|
</shuxcc:VerticalCard.Bottom>
|
||||||
</shuxcc:VerticalCard>
|
</shuxcc:VerticalCard>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
@@ -143,7 +145,7 @@
|
|||||||
</shuxcc:VerticalCard>
|
</shuxcc:VerticalCard>
|
||||||
<ItemsControl
|
<ItemsControl
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
VerticalAlignment="Bottom"
|
VerticalAlignment="Stretch"
|
||||||
ItemTemplate="{StaticResource AvatarGridViewSkillTemplate}"
|
ItemTemplate="{StaticResource AvatarGridViewSkillTemplate}"
|
||||||
ItemsPanel="{StaticResource HorizontalStackPanelSpacing6Template}"
|
ItemsPanel="{StaticResource HorizontalStackPanelSpacing6Template}"
|
||||||
ItemsSource="{Binding Skills}"/>
|
ItemsSource="{Binding Skills}"/>
|
||||||
@@ -252,7 +254,7 @@
|
|||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="auto"/>
|
<ColumnDefinition Width="auto"/>
|
||||||
<ColumnDefinition Width="48"/>
|
<ColumnDefinition Width="100"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
<shuxci:CachedImage
|
<shuxci:CachedImage
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
@@ -266,7 +268,7 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Foreground="#FFFFFFFF"
|
Foreground="#FFFFFFFF"
|
||||||
Style="{StaticResource BaseTextBlockStyle}"
|
Style="{StaticResource BaseTextBlockStyle}"
|
||||||
Text="{Binding Info.Level}"/>
|
Text="{Binding Level}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Button.Content>
|
</Button.Content>
|
||||||
<Button.Flyout>
|
<Button.Flyout>
|
||||||
|
|||||||
@@ -357,7 +357,7 @@
|
|||||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
||||||
Padding="10,8,0,0"
|
Padding="10,8,0,0"
|
||||||
Maximum="720"
|
Maximum="720"
|
||||||
Minimum="60"
|
Minimum="-1"
|
||||||
SpinButtonPlacementMode="Inline"
|
SpinButtonPlacementMode="Inline"
|
||||||
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
|
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
@@ -367,23 +367,22 @@
|
|||||||
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
|
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<cwc:SettingsExpander.Items>
|
<cwc:SettingsExpander.Items>
|
||||||
<cwc:SettingsCard Description="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsKindDescription}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsKindHeader}">
|
<cwc:SettingsCard Description="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovDescription}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHeader}">
|
||||||
<StackPanel VerticalAlignment="Center" Spacing="3">
|
<NumberBox
|
||||||
<TextBlock
|
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
||||||
HorizontalAlignment="Right"
|
Padding="10,8,0,0"
|
||||||
Foreground="{ThemeResource SystemControlErrorTextForegroundBrush}"
|
Maximum="55"
|
||||||
Opacity="0.8"
|
Minimum="45"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
SmallChange="1"
|
||||||
Text="{Binding LaunchOptions.UnlockerKind.Description, Mode=OneWay}"/>
|
SpinButtonPlacementMode="Inline"
|
||||||
<shuxc:SizeRestrictedContentControl HorizontalAlignment="Right">
|
Value="{Binding LaunchOptions.TargetFov, Mode=TwoWay}"/>
|
||||||
<ComboBox
|
</cwc:SettingsCard>
|
||||||
DisplayMemberPath="Name"
|
<cwc:SettingsCard Description="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogDescription}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogHeader}">
|
||||||
ItemsSource="{Binding LaunchOptions.UnlockerKinds, Mode=OneWay}"
|
<ToggleSwitch
|
||||||
SelectedItem="{Binding LaunchOptions.UnlockerKind, Mode=TwoWay}"/>
|
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
|
||||||
</shuxc:SizeRestrictedContentControl>
|
IsOn="{Binding LaunchOptions.DisableFog, Mode=TwoWay}"
|
||||||
|
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
||||||
</StackPanel>
|
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"/>
|
||||||
|
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
</cwc:SettingsExpander.Items>
|
</cwc:SettingsExpander.Items>
|
||||||
</cwc:SettingsExpander>
|
</cwc:SettingsExpander>
|
||||||
|
|||||||
@@ -120,6 +120,10 @@
|
|||||||
<ToggleSwitch IsOn="{Binding AlwaysIsFirstRunAfterUpdate, Mode=TwoWay}"/>
|
<ToggleSwitch IsOn="{Binding AlwaysIsFirstRunAfterUpdate, Mode=TwoWay}"/>
|
||||||
</cwc:SettingsCard>
|
</cwc:SettingsCard>
|
||||||
|
|
||||||
|
<cwc:SettingsCard Header="Alpha Build Use CN Patch Endpoint">
|
||||||
|
<ToggleSwitch IsOn="{Binding AlphaBuildUseCNPatchEndpoint, Mode=TwoWay}"/>
|
||||||
|
</cwc:SettingsCard>
|
||||||
|
|
||||||
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="Gacha Service"/>
|
<TextBlock Style="{ThemeResource SettingsSectionHeaderTextBlockStyle}" Text="Gacha Service"/>
|
||||||
<cwc:SettingsCard
|
<cwc:SettingsCard
|
||||||
Command="{Binding CompensationGachaLogServiceTimeCommand}"
|
Command="{Binding CompensationGachaLogServiceTimeCommand}"
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ internal static class WindowExtension
|
|||||||
ShowWindow(window.GetWindowHandle(), SHOW_WINDOW_CMD.SW_HIDE);
|
ShowWindow(window.GetWindowHandle(), SHOW_WINDOW_CMD.SW_HIDE);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetLayered(this Window window, bool full = true)
|
public static void SetExStyleLayered(this Window window, bool full = true)
|
||||||
{
|
{
|
||||||
HWND hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
HWND hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
||||||
nint style = GetWindowLongPtrW(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
|
nint style = GetWindowLongPtrW(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
|
||||||
@@ -97,7 +97,7 @@ internal static class WindowExtension
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetToolWindow(this Window window)
|
public static void SetExStyleToolWindow(this Window window)
|
||||||
{
|
{
|
||||||
HWND hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
HWND hwnd = (HWND)WindowNative.GetWindowHandle(window);
|
||||||
nint style = GetWindowLongPtrW(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
|
nint style = GetWindowLongPtrW(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ internal sealed class SkillView : NameIconDescription, ITypedCalculableSource<IC
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 不计算命座的技能等级字符串
|
/// 不计算命座的技能等级字符串
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string Level { get => LevelFormat.Format(LevelNumber); }
|
public string Level { get; set; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 技能组Id
|
/// 技能组Id
|
||||||
|
|||||||
@@ -20,10 +20,6 @@ using System.IO;
|
|||||||
|
|
||||||
namespace Snap.Hutao.ViewModel;
|
namespace Snap.Hutao.ViewModel;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 测试视图模型
|
|
||||||
/// </summary>
|
|
||||||
[HighQuality]
|
|
||||||
[ConstructorGenerated]
|
[ConstructorGenerated]
|
||||||
[Injection(InjectAs.Scoped)]
|
[Injection(InjectAs.Scoped)]
|
||||||
internal sealed partial class TestViewModel : Abstraction.ViewModel
|
internal sealed partial class TestViewModel : Abstraction.ViewModel
|
||||||
@@ -110,6 +106,20 @@ internal sealed partial class TestViewModel : Abstraction.ViewModel
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool AlphaBuildUseCNPatchEndpoint
|
||||||
|
{
|
||||||
|
get => LocalSetting.Get(SettingKeys.AlphaBuildUseCNPatchEndpoint, false);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (IsViewDisposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LocalSetting.Set(SettingKeys.AlphaBuildUseCNPatchEndpoint, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Command("ResetGuideStateCommand")]
|
[Command("ResetGuideStateCommand")]
|
||||||
private static void ResetGuideState()
|
private static void ResetGuideState()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ using Snap.Hutao.UI.Xaml.Behavior.Action;
|
|||||||
using Snap.Hutao.UI.Xaml.Control;
|
using Snap.Hutao.UI.Xaml.Control;
|
||||||
using Snap.Hutao.UI.Xaml.View.Dialog;
|
using Snap.Hutao.UI.Xaml.View.Dialog;
|
||||||
using Snap.Hutao.UI.Xaml.View.Window.WebView2;
|
using Snap.Hutao.UI.Xaml.View.Window.WebView2;
|
||||||
|
using Snap.Hutao.Web.Hutao;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -94,11 +95,23 @@ internal sealed partial class TitleViewModel : Abstraction.ViewModel
|
|||||||
await taskContext.SwitchToMainThreadAsync();
|
await taskContext.SwitchToMainThreadAsync();
|
||||||
|
|
||||||
dialog.Title = SH.FormatViewTitileUpdatePackageDownloadTitle(UpdateStatus?.Version);
|
dialog.Title = SH.FormatViewTitileUpdatePackageDownloadTitle(UpdateStatus?.Version);
|
||||||
|
dialog.Mirrors = checkUpdateResult.PackageInformation?.Mirrors;
|
||||||
|
dialog.SelectedItem = dialog.Mirrors?.FirstOrDefault();
|
||||||
|
|
||||||
if (await dialog.ShowAsync() is ContentDialogResult.Primary)
|
(bool isOk, HutaoPackageMirror? mirror) = await dialog.GetSelectedMirrorAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (isOk && mirror is not null)
|
||||||
{
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(checkUpdateResult.PackageInformation);
|
||||||
|
HutaoSelectedMirrorInformation mirrorInformation = new()
|
||||||
|
{
|
||||||
|
Mirror = mirror,
|
||||||
|
Validation = checkUpdateResult.PackageInformation.Validation,
|
||||||
|
Version = checkUpdateResult.PackageInformation.Version,
|
||||||
|
};
|
||||||
|
|
||||||
// This method will set CheckUpdateResult.Kind to NeedInstall if download success
|
// This method will set CheckUpdateResult.Kind to NeedInstall if download success
|
||||||
if (!await DownloadPackageAsync(progress, checkUpdateResult).ConfigureAwait(false))
|
if (!await DownloadPackageAsync(progress, mirrorInformation, checkUpdateResult).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
infoBarService.Warning(SH.ViewTitileUpdatePackageDownloadFailedMessage);
|
infoBarService.Warning(SH.ViewTitileUpdatePackageDownloadFailedMessage);
|
||||||
return;
|
return;
|
||||||
@@ -117,7 +130,7 @@ internal sealed partial class TitleViewModel : Abstraction.ViewModel
|
|||||||
|
|
||||||
if (installUpdateUserConsentResult is ContentDialogResult.Primary)
|
if (installUpdateUserConsentResult is ContentDialogResult.Primary)
|
||||||
{
|
{
|
||||||
LaunchUpdaterResult launchUpdaterResult = await updateService.LaunchUpdaterAsync().ConfigureAwait(false);
|
LaunchUpdaterResult launchUpdaterResult = updateService.LaunchUpdater();
|
||||||
if (launchUpdaterResult.IsSuccess)
|
if (launchUpdaterResult.IsSuccess)
|
||||||
{
|
{
|
||||||
ContentDialog contentDialog = await contentDialogFactory
|
ContentDialog contentDialog = await contentDialogFactory
|
||||||
@@ -138,12 +151,12 @@ internal sealed partial class TitleViewModel : Abstraction.ViewModel
|
|||||||
UpdateStatus = null;
|
UpdateStatus = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<bool> DownloadPackageAsync(IProgress<UpdateStatus> progress, CheckUpdateResult checkUpdateResult)
|
private async ValueTask<bool> DownloadPackageAsync(IProgress<UpdateStatus> progress, HutaoSelectedMirrorInformation mirrorInformation, CheckUpdateResult checkUpdateResult)
|
||||||
{
|
{
|
||||||
bool downloadSuccess = true;
|
bool downloadSuccess = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (await updateService.DownloadUpdateAsync(checkUpdateResult, progress).ConfigureAwait(false))
|
if (await updateService.DownloadUpdateAsync(mirrorInformation, progress).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
checkUpdateResult.Kind = CheckUpdateResultKind.NeedInstall;
|
checkUpdateResult.Kind = CheckUpdateResultKind.NeedInstall;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,116 +7,51 @@ using Snap.Hutao.Web.Hoyolab;
|
|||||||
|
|
||||||
namespace Snap.Hutao.Web;
|
namespace Snap.Hutao.Web;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 国服 API 端点
|
|
||||||
/// </summary>
|
|
||||||
[HighQuality]
|
|
||||||
[SuppressMessage("", "SA1201")]
|
[SuppressMessage("", "SA1201")]
|
||||||
[SuppressMessage("", "SA1202")]
|
[SuppressMessage("", "SA1202")]
|
||||||
internal static class ApiEndpoints
|
internal static class ApiEndpoints
|
||||||
{
|
{
|
||||||
#region ApiTakumiAuthApi
|
#region ApiTakumiAuthApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 stoken 与 ltoken
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="actionType">操作类型 game_role</param>
|
|
||||||
/// <param name="stoken">SToken</param>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>Url</returns>
|
|
||||||
public static string AuthActionTicket(string actionType, string stoken, string uid)
|
public static string AuthActionTicket(string actionType, string stoken, string uid)
|
||||||
{
|
{
|
||||||
return $"{ApiTakumiAuthApi}/getActionTicketBySToken?action_type={actionType}&stoken={Uri.EscapeDataString(stoken)}&uid={uid}";
|
return $"{ApiTakumiAuthApi}/getActionTicketBySToken?action_type={actionType}&stoken={Uri.EscapeDataString(stoken)}&uid={uid}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 stoken 与 ltoken
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="loginTicket">登录票证</param>
|
|
||||||
/// <param name="loginUid">uid</param>
|
|
||||||
/// <returns>Url</returns>
|
|
||||||
public static string AuthMultiToken(string loginTicket, string loginUid)
|
public static string AuthMultiToken(string loginTicket, string loginUid)
|
||||||
{
|
{
|
||||||
return $"{ApiTakumiAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
|
return $"{ApiTakumiAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ApiTaKumiBindingApi
|
#region ApiTaKumiBindingApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户游戏角色
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="actionTicket">操作凭证</param>
|
|
||||||
/// <returns>用户游戏角色字符串</returns>
|
|
||||||
public static string UserGameRolesByActionTicket(string actionTicket)
|
public static string UserGameRolesByActionTicket(string actionTicket)
|
||||||
{
|
{
|
||||||
return $"{ApiTaKumiBindingApi}/getUserGameRoles?action_ticket={actionTicket}&game_biz=hk4e_cn";
|
return $"{ApiTaKumiBindingApi}/getUserGameRoles?action_ticket={actionTicket}&game_biz=hk4e_cn";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户游戏角色
|
|
||||||
/// </summary>
|
|
||||||
public const string UserGameRolesByCookie = $"{ApiTaKumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_cn";
|
public const string UserGameRolesByCookie = $"{ApiTaKumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_cn";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户游戏角色
|
|
||||||
/// </summary>
|
|
||||||
public const string UserGameRolesBySToken = $"{ApiTaKumiBindingApi}/getUserGameRolesByStoken";
|
public const string UserGameRolesBySToken = $"{ApiTaKumiBindingApi}/getUserGameRolesByStoken";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// AuthKey
|
|
||||||
/// </summary>
|
|
||||||
public const string BindingGenAuthKey = $"{ApiTaKumiBindingApi}/genAuthKey";
|
public const string BindingGenAuthKey = $"{ApiTaKumiBindingApi}/genAuthKey";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ApiTakumiCardApi | ApiTakumiRecordApi
|
#region ApiTakumiCardApi | ApiTakumiRecordApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 小组件数据
|
|
||||||
/// </summary>
|
|
||||||
public const string CardWidgetData = $"{ApiTakumiCardApi}/getWidgetData?game_id=2";
|
public const string CardWidgetData = $"{ApiTakumiCardApi}/getWidgetData?game_id=2";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 小组件数据v2
|
|
||||||
/// </summary>
|
|
||||||
public const string CardWidgetData2 = $"{ApiTakumiRecordAapi}/widget/v2?game_id=2";
|
public const string CardWidgetData2 = $"{ApiTakumiRecordAapi}/widget/v2?game_id=2";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 发起验证码
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="highRisk">是否为高风险</param>
|
|
||||||
/// <returns>发起验证码Url</returns>
|
|
||||||
public static string CardCreateVerification(bool highRisk)
|
public static string CardCreateVerification(bool highRisk)
|
||||||
{
|
{
|
||||||
return $"{ApiTakumiCardWApi}/createVerification?is_high={(highRisk ? "true" : "false")}";
|
return $"{ApiTakumiCardWApi}/createVerification?is_high={(highRisk ? "true" : "false")}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 验证验证码
|
|
||||||
/// </summary>
|
|
||||||
public const string CardVerifyVerification = $"{ApiTakumiCardWApi}/verifyVerification";
|
public const string CardVerifyVerification = $"{ApiTakumiCardWApi}/verifyVerification";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 角色基本信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>角色基本信息字符串</returns>
|
|
||||||
public static string GameRecordRoleBasicInfo(in PlayerUid uid)
|
public static string GameRecordRoleBasicInfo(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{ApiTakumiRecordApi}/roleBasicInfo?role_id={uid.Value}&server={uid.Region}";
|
return $"{ApiTakumiRecordApi}/roleBasicInfo?role_id={uid.Value}&server={uid.Region}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 角色信息
|
|
||||||
/// </summary>
|
|
||||||
public const string GameRecordCharacter = $"{ApiTakumiRecordApi}/character";
|
public const string GameRecordCharacter = $"{ApiTakumiRecordApi}/character";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 游戏记录实时便笺
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>游戏记录实时便笺字符串</returns>
|
|
||||||
public static string GameRecordDailyNote(in PlayerUid uid)
|
public static string GameRecordDailyNote(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{GameRecordDailyNotePath}?server={uid.Region}&role_id={uid.Value}";
|
return $"{GameRecordDailyNotePath}?server={uid.Region}&role_id={uid.Value}";
|
||||||
@@ -124,11 +59,6 @@ internal static class ApiEndpoints
|
|||||||
|
|
||||||
public const string GameRecordDailyNotePath = $"{ApiTakumiRecordApi}/dailyNote";
|
public const string GameRecordDailyNotePath = $"{ApiTakumiRecordApi}/dailyNote";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 游戏记录主页
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>游戏记录主页字符串</returns>
|
|
||||||
public static string GameRecordIndex(in PlayerUid uid)
|
public static string GameRecordIndex(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{GameRecordIndexPath}?server={uid.Region}&role_id={uid.Value}";
|
return $"{GameRecordIndexPath}?server={uid.Region}&role_id={uid.Value}";
|
||||||
@@ -136,12 +66,6 @@ internal static class ApiEndpoints
|
|||||||
|
|
||||||
public const string GameRecordIndexPath = $"{ApiTakumiRecordApi}/index";
|
public const string GameRecordIndexPath = $"{ApiTakumiRecordApi}/index";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 深渊信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scheduleType">深渊类型</param>
|
|
||||||
/// <param name="uid">Uid</param>
|
|
||||||
/// <returns>深渊信息字符串</returns>
|
|
||||||
public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.ScheduleType scheduleType, in PlayerUid uid)
|
public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.ScheduleType scheduleType, in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{GameRecordSpiralAbyssPath}?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
|
return $"{GameRecordSpiralAbyssPath}?schedule_type={(int)scheduleType}&role_id={uid.Value}&server={uid.Region}";
|
||||||
@@ -227,30 +151,13 @@ internal static class ApiEndpoints
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region AppAuthApi
|
#region AppAuthApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 另一个AuthKey
|
|
||||||
/// </summary>
|
|
||||||
public const string AppAuthGenAuthKey = $"{AppAuthApi}/genAuthKey";
|
public const string AppAuthGenAuthKey = $"{AppAuthApi}/genAuthKey";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region BbsApiUserApi
|
#region BbsApiUserApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// BBS 指向引用
|
|
||||||
/// </summary>
|
|
||||||
public const string BbsReferer = "https://bbs.mihoyo.com/";
|
public const string BbsReferer = "https://bbs.mihoyo.com/";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户详细信息
|
|
||||||
/// </summary>
|
|
||||||
public const string UserFullInfo = $"{BbsApiUserApi}/getUserFullInfo?gids=2";
|
public const string UserFullInfo = $"{BbsApiUserApi}/getUserFullInfo?gids=2";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查询其他用户详细信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bbsUid">bbs Uid</param>
|
|
||||||
/// <returns>查询其他用户详细信息字符串</returns>
|
|
||||||
public static string UserFullInfoQuery(string bbsUid)
|
public static string UserFullInfoQuery(string bbsUid)
|
||||||
{
|
{
|
||||||
return $"{BbsApiUserApi}/getUserFullInfo?uid={bbsUid}&gids=2";
|
return $"{BbsApiUserApi}/getUserFullInfo?uid={bbsUid}&gids=2";
|
||||||
@@ -258,24 +165,11 @@ internal static class ApiEndpoints
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Hk4eApiAnnouncementApi
|
#region Hk4eApiAnnouncementApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 公告列表
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="languageCode">语言代码</param>
|
|
||||||
/// <param name="region">服务器</param>
|
|
||||||
/// <returns>公告列表Url</returns>
|
|
||||||
public static string AnnList(string languageCode, in Region region)
|
public static string AnnList(string languageCode, in Region region)
|
||||||
{
|
{
|
||||||
return $"{Hk4eApiAnnouncementApi}/getAnnList?{AnnouncementQuery(languageCode, region)}";
|
return $"{Hk4eApiAnnouncementApi}/getAnnList?{AnnouncementQuery(languageCode, region)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 公告内容
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="languageCode">语言代码</param>
|
|
||||||
/// <param name="region">服务器</param>
|
|
||||||
/// <returns>公告列表Url</returns>
|
|
||||||
public static string AnnContent(string languageCode, in Region region)
|
public static string AnnContent(string languageCode, in Region region)
|
||||||
{
|
{
|
||||||
return $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery(languageCode, region)}";
|
return $"{Hk4eApiAnnouncementApi}/getAnnContent?{AnnouncementQuery(languageCode, region)}";
|
||||||
@@ -283,15 +177,11 @@ internal static class ApiEndpoints
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Hk4eSdk
|
#region Hk4eSdk
|
||||||
|
|
||||||
public const string QrCodeFetch = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/fetch";
|
public const string QrCodeFetch = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/fetch";
|
||||||
|
|
||||||
public const string QrCodeQuery = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/query";
|
public const string QrCodeQuery = $"{Hk4eSdk}/hk4e_cn/combo/panda/qrcode/query";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region HoyoPlayApi
|
#region HoyoPlayApi
|
||||||
|
|
||||||
public static string HoyoPlayConnectGamePackages(LaunchScheme scheme)
|
public static string HoyoPlayConnectGamePackages(LaunchScheme scheme)
|
||||||
{
|
{
|
||||||
return $"{HoyoPlayApiConnectApi}/getGamePackages?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
|
return $"{HoyoPlayApiConnectApi}/getGamePackages?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
|
||||||
@@ -306,52 +196,19 @@ internal static class ApiEndpoints
|
|||||||
{
|
{
|
||||||
return $"{HoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
|
return $"{HoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region PassportApi | PassportApiV4
|
#region PassportApi | PassportApiV4
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 CookieToken
|
|
||||||
/// </summary>
|
|
||||||
public const string AccountGetCookieTokenBySToken = $"{PassportApiAuthApi}/getCookieAccountInfoBySToken";
|
public const string AccountGetCookieTokenBySToken = $"{PassportApiAuthApi}/getCookieAccountInfoBySToken";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取LToken
|
|
||||||
/// </summary>
|
|
||||||
public const string AccountGetLTokenBySToken = $"{PassportApiAuthApi}/getLTokenBySToken";
|
public const string AccountGetLTokenBySToken = $"{PassportApiAuthApi}/getLTokenBySToken";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 通过GameToken获取V2SToken
|
|
||||||
/// </summary>
|
|
||||||
public const string AccountGetSTokenByGameToken = $"{PassportApi}/account/ma-cn-session/app/getTokenByGameToken";
|
public const string AccountGetSTokenByGameToken = $"{PassportApi}/account/ma-cn-session/app/getTokenByGameToken";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取V2SToken
|
|
||||||
/// </summary>
|
|
||||||
public const string AccountGetSTokenByOldToken = $"{PassportApi}/account/ma-cn-session/app/getTokenBySToken";
|
public const string AccountGetSTokenByOldToken = $"{PassportApi}/account/ma-cn-session/app/getTokenBySToken";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 登录
|
|
||||||
/// </summary>
|
|
||||||
public const string AccountLoginByPassword = $"{PassportApi}/account/ma-cn-passport/app/loginByPassword";
|
public const string AccountLoginByPassword = $"{PassportApi}/account/ma-cn-passport/app/loginByPassword";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 验证 Ltoken 有效性
|
|
||||||
/// </summary>
|
|
||||||
public const string AccountVerifyLtoken = $"{PassportApiV4}/account/ma-cn-session/web/verifyLtoken";
|
public const string AccountVerifyLtoken = $"{PassportApiV4}/account/ma-cn-session/web/verifyLtoken";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 创建 ActionTicket
|
|
||||||
/// </summary>
|
|
||||||
public const string AccountCreateActionTicket = $"{PassportApi}/account/ma-cn-verifier/app/createActionTicketByToken";
|
public const string AccountCreateActionTicket = $"{PassportApi}/account/ma-cn-verifier/app/createActionTicketByToken";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region PublicDataApi
|
#region PublicDataApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 fingerprint
|
|
||||||
/// </summary>
|
|
||||||
public const string DeviceFpGetFp = $"{PublicDataApiDeviceFpApi}/getFp";
|
public const string DeviceFpGetFp = $"{PublicDataApiDeviceFpApi}/getFp";
|
||||||
|
|
||||||
public static string DeviceFpGetExtList(int platform)
|
public static string DeviceFpGetExtList(int platform)
|
||||||
@@ -361,12 +218,6 @@ internal static class ApiEndpoints
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region PublicOperationHk4eGachaInfoApi
|
#region PublicOperationHk4eGachaInfoApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取祈愿记录
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">query string</param>
|
|
||||||
/// <returns>祈愿记录信息Url</returns>
|
|
||||||
public static string GachaInfoGetGachaLog(string query)
|
public static string GachaInfoGetGachaLog(string query)
|
||||||
{
|
{
|
||||||
return $"{PublicOperationHk4eGachaInfoApi}/getGachaLog?{query}";
|
return $"{PublicOperationHk4eGachaInfoApi}/getGachaLog?{query}";
|
||||||
@@ -413,9 +264,6 @@ internal static class ApiEndpoints
|
|||||||
private const string PublicOperationHk4e = "https://public-operation-hk4e.mihoyo.com";
|
private const string PublicOperationHk4e = "https://public-operation-hk4e.mihoyo.com";
|
||||||
private const string PublicOperationHk4eGachaInfoApi = $"{PublicOperationHk4e}/gacha_info/api";
|
private const string PublicOperationHk4eGachaInfoApi = $"{PublicOperationHk4e}/gacha_info/api";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Referer: https://webstatic.mihoyo.com
|
|
||||||
/// </summary>
|
|
||||||
public const string WebStaticMihoyoReferer = "https://webstatic.mihoyo.com";
|
public const string WebStaticMihoyoReferer = "https://webstatic.mihoyo.com";
|
||||||
|
|
||||||
private static string AnnouncementQuery(string languageCode, in Region region)
|
private static string AnnouncementQuery(string languageCode, in Region region)
|
||||||
|
|||||||
@@ -7,181 +7,71 @@ using Snap.Hutao.Web.Hoyolab;
|
|||||||
|
|
||||||
namespace Snap.Hutao.Web;
|
namespace Snap.Hutao.Web;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 国际服 API 端点
|
|
||||||
/// </summary>
|
|
||||||
[HighQuality]
|
|
||||||
[SuppressMessage("", "SA1201")]
|
[SuppressMessage("", "SA1201")]
|
||||||
[SuppressMessage("", "SA1202")]
|
[SuppressMessage("", "SA1202")]
|
||||||
internal static class ApiOsEndpoints
|
internal static class ApiOsEndpoints
|
||||||
{
|
{
|
||||||
#region ApiAccountOsApi
|
#region ApiAccountOsApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Hoyolab App Login api
|
|
||||||
/// Can fetch stoken
|
|
||||||
/// </summary>
|
|
||||||
public const string WebLoginByPassword = $"{ApiAccountOsAuthApi}/webLoginByPassword";
|
public const string WebLoginByPassword = $"{ApiAccountOsAuthApi}/webLoginByPassword";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 Ltoken
|
|
||||||
/// </summary>
|
|
||||||
public const string AccountGetLTokenBySToken = $"{ApiAccountOsAuthApi}/getLTokenBySToken";
|
public const string AccountGetLTokenBySToken = $"{ApiAccountOsAuthApi}/getLTokenBySToken";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// fetch CookieToken
|
|
||||||
/// </summary>
|
|
||||||
public const string AccountGetCookieTokenBySToken = $"{ApiAccountOsAuthApi}/getCookieAccountInfoBySToken";
|
public const string AccountGetCookieTokenBySToken = $"{ApiAccountOsAuthApi}/getCookieAccountInfoBySToken";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ApiGeetest
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取GT码
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="gt">gt</param>
|
|
||||||
/// <returns>GT码Url</returns>
|
|
||||||
public static string GeetestGetType(string gt)
|
|
||||||
{
|
|
||||||
return $"{ApiNaGeetest}/gettype.php?gt={gt}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 验证接口
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="gt">gt</param>
|
|
||||||
/// <param name="challenge">challenge流水号</param>
|
|
||||||
/// <returns>验证接口Url</returns>
|
|
||||||
public static string GeetestAjax(string gt, string challenge)
|
|
||||||
{
|
|
||||||
return $"{ApiNaGeetest}/ajax.php?gt={gt}&challenge={challenge}&lang=zh-cn&pt=0&client_type=web";
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region ApiOsTakumiAuthApi
|
#region ApiOsTakumiAuthApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 stoken 与 ltoken
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="loginTicket">登录票证</param>
|
|
||||||
/// <param name="loginUid">uid</param>
|
|
||||||
/// <returns>Url</returns>
|
|
||||||
public static string AuthMultiToken(string loginTicket, string loginUid)
|
public static string AuthMultiToken(string loginTicket, string loginUid)
|
||||||
{
|
{
|
||||||
return $"{ApiAccountOsAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
|
return $"{ApiAccountOsAuthApi}/getMultiTokenByLoginTicket?login_ticket={loginTicket}&uid={loginUid}&token_types=3";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取 stoken 与 ltoken
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="actionType">操作类型 game_role</param>
|
|
||||||
/// <param name="stoken">SToken</param>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>Url</returns>
|
|
||||||
public static string AuthActionTicket(string actionType, string stoken, string uid)
|
public static string AuthActionTicket(string actionType, string stoken, string uid)
|
||||||
{
|
{
|
||||||
return $"{ApiAccountOsAuthApi}/getActionTicketBySToken?action_type={actionType}&stoken={Uri.EscapeDataString(stoken)}&uid={uid}";
|
return $"{ApiAccountOsAuthApi}/getActionTicketBySToken?action_type={actionType}&stoken={Uri.EscapeDataString(stoken)}&uid={uid}";
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region ApiOsTakumiBindingApi
|
#region ApiOsTakumiBindingApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户游戏角色
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>用户游戏角色字符串</returns>
|
|
||||||
public const string UserGameRolesByCookie = $"{ApiOsTakumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_global";
|
public const string UserGameRolesByCookie = $"{ApiOsTakumiBindingApi}/getUserGameRolesByCookie?game_biz=hk4e_global";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户游戏角色
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="region">地区代号</param>
|
|
||||||
/// <returns>用户游戏角色字符串</returns>
|
|
||||||
public static string UserGameRolesByLtoken(in Region region)
|
public static string UserGameRolesByLtoken(in Region region)
|
||||||
{
|
{
|
||||||
return $"{ApiAccountOsBindingApi}/getUserGameRolesByLtoken?game_biz=hk4e_global®ion={region}";
|
return $"{ApiAccountOsBindingApi}/getUserGameRolesByLtoken?game_biz=hk4e_global®ion={region}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Game Authkey
|
|
||||||
/// </summary>
|
|
||||||
public const string BindingGenAuthKey = $"{ApiAccountOsBindingApi}/genAuthKey";
|
public const string BindingGenAuthKey = $"{ApiAccountOsBindingApi}/genAuthKey";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region BbsApiOsApi
|
#region BbsApiOsApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 查询其他用户详细信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="bbsUid">bbs Uid</param>
|
|
||||||
/// <returns>查询其他用户详细信息字符串</returns>
|
|
||||||
public static string UserFullInfoQuery(string bbsUid)
|
public static string UserFullInfoQuery(string bbsUid)
|
||||||
{
|
{
|
||||||
return $"{BbsApiOs}/community/painter/wapi/user/full";
|
return $"{BbsApiOs}/community/painter/wapi/user/full";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户详细信息
|
|
||||||
/// </summary>
|
|
||||||
public const string UserFullInfo = $"{BbsApiOs}/community/user/wapi/getUserFullInfo?gid=2";
|
public const string UserFullInfo = $"{BbsApiOs}/community/user/wapi/getUserFullInfo?gid=2";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 国际服角色基本信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>角色基本信息字符串</returns>
|
|
||||||
public static string GameRecordRoleBasicInfo(in PlayerUid uid)
|
public static string GameRecordRoleBasicInfo(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{BbsApiOsGameRecordAppApi}/roleBasicInfo?role_id={uid.Value}&server={uid.Region}";
|
return $"{BbsApiOsGameRecordAppApi}/roleBasicInfo?role_id={uid.Value}&server={uid.Region}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 国际服角色信息
|
|
||||||
/// </summary>
|
|
||||||
public const string GameRecordCharacter = $"{BbsApiOsGameRecordAppApi}/character";
|
public const string GameRecordCharacter = $"{BbsApiOsGameRecordAppApi}/character";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 国际服游戏记录实时便笺
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>游戏记录实时便笺字符串</returns>
|
|
||||||
public static string GameRecordDailyNote(in PlayerUid uid)
|
public static string GameRecordDailyNote(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{BbsApiOsGameRecordAppApi}/dailyNote?server={uid.Region}&role_id={uid.Value}";
|
return $"{BbsApiOsGameRecordAppApi}/dailyNote?server={uid.Region}&role_id={uid.Value}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 国际服游戏记录主页
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>游戏记录主页字符串</returns>
|
|
||||||
public static string GameRecordIndex(in PlayerUid uid)
|
public static string GameRecordIndex(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{BbsApiOsGameRecordAppApi}/index?server={uid.Region}&role_id={uid.Value}";
|
return $"{BbsApiOsGameRecordAppApi}/index?server={uid.Region}&role_id={uid.Value}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 国际服深渊信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="scheduleType">深渊类型</param>
|
|
||||||
/// <param name="uid">Uid</param>
|
|
||||||
/// <returns>深渊信息字符串</returns>
|
|
||||||
public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.ScheduleType scheduleType, in PlayerUid uid)
|
public static string GameRecordSpiralAbyss(Hoyolab.Takumi.GameRecord.ScheduleType scheduleType, in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{BbsApiOsGameRecordAppApi}/spiralAbyss?server={uid.Region}&role_id={uid.Value}&schedule_type={(int)scheduleType}";
|
return $"{BbsApiOsGameRecordAppApi}/spiralAbyss?server={uid.Region}&role_id={uid.Value}&schedule_type={(int)scheduleType}";
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Hk4eApiOsGachaInfoApi
|
#region Hk4eApiOsGachaInfoApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取祈愿记录
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="query">query string</param>
|
|
||||||
/// <returns>祈愿记录信息Url</returns>
|
|
||||||
public static string GachaInfoGetGachaLog(string query)
|
public static string GachaInfoGetGachaLog(string query)
|
||||||
{
|
{
|
||||||
return $"{Hk4eApiOsGachaInfoApi}/getGachaLog?{query}";
|
return $"{Hk4eApiOsGachaInfoApi}/getGachaLog?{query}";
|
||||||
@@ -189,24 +79,11 @@ internal static class ApiOsEndpoints
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Hk4eApiOsAnnouncementApi
|
#region Hk4eApiOsAnnouncementApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 公告列表
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="languageCode">语言代码</param>
|
|
||||||
/// <param name="region">服务器</param>
|
|
||||||
/// <returns>公告列表Url</returns>
|
|
||||||
public static string AnnList(string languageCode, in Region region)
|
public static string AnnList(string languageCode, in Region region)
|
||||||
{
|
{
|
||||||
return $"{Hk4eApiOsAnnouncementApi}/getAnnList?{AnnouncementQuery(languageCode, region)}";
|
return $"{Hk4eApiOsAnnouncementApi}/getAnnList?{AnnouncementQuery(languageCode, region)}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 公告内容
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="languageCode">语言代码</param>
|
|
||||||
/// <param name="region">服务器</param>
|
|
||||||
/// <returns>公告内容Url</returns>
|
|
||||||
public static string AnnContent(string languageCode, in Region region)
|
public static string AnnContent(string languageCode, in Region region)
|
||||||
{
|
{
|
||||||
return $"{Hk4eApiOsAnnouncementApi}/getAnnContent?{AnnouncementQuery(languageCode, region)}";
|
return $"{Hk4eApiOsAnnouncementApi}/getAnnContent?{AnnouncementQuery(languageCode, region)}";
|
||||||
@@ -215,84 +92,31 @@ internal static class ApiOsEndpoints
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region SgPublicApi
|
#region SgPublicApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 计算器家具计算
|
|
||||||
/// </summary>
|
|
||||||
public const string CalculateFurnitureCompute = $"{SgPublicApi}/event/calculateos/furniture/list";
|
public const string CalculateFurnitureCompute = $"{SgPublicApi}/event/calculateos/furniture/list";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 计算器角色列表 size 20
|
|
||||||
/// </summary>
|
|
||||||
public const string CalculateAvatarList = $"{SgPublicApi}/event/calculateos/avatar/list";
|
public const string CalculateAvatarList = $"{SgPublicApi}/event/calculateos/avatar/list";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 计算器武器列表 size 20
|
|
||||||
/// </summary>
|
|
||||||
public const string CalculateWeaponList = $"{SgPublicApi}/event/calculateos/weapon/list";
|
public const string CalculateWeaponList = $"{SgPublicApi}/event/calculateos/weapon/list";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 计算器结果
|
|
||||||
/// </summary>
|
|
||||||
public const string CalculateCompute = $"{SgPublicApi}/event/calculateos/compute";
|
public const string CalculateCompute = $"{SgPublicApi}/event/calculateos/compute";
|
||||||
|
|
||||||
public const string CalculateBatchCompute = $"{SgPublicApi}/event/calculateos/batch_compute";
|
public const string CalculateBatchCompute = $"{SgPublicApi}/event/calculateos/batch_compute";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 计算器同步角色详情 size 20
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="avatarId">角色Id</param>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>角色详情</returns>
|
|
||||||
public static string CalculateSyncAvatarDetail(in AvatarId avatarId, in PlayerUid uid)
|
public static string CalculateSyncAvatarDetail(in AvatarId avatarId, in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{SgPublicApi}/event/calculateos/sync/avatar/detail?avatar_id={avatarId.Value}&uid={uid.Value}®ion={uid.Region}";
|
return $"{SgPublicApi}/event/calculateos/sync/avatar/detail?avatar_id={avatarId.Value}&uid={uid.Value}®ion={uid.Region}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 计算器同步角色列表 size 20
|
|
||||||
/// </summary>
|
|
||||||
public const string CalculateSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
|
public const string CalculateSyncAvatarList = $"{SgPublicApi}/event/calculateos/sync/avatar/list";
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region SgHk4eApi
|
#region SgHk4eApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 签到活动Id
|
|
||||||
/// </summary>
|
|
||||||
public const string SignInRewardActivityId = "e202102251931481";
|
public const string SignInRewardActivityId = "e202102251931481";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 签到
|
|
||||||
/// </summary>
|
|
||||||
public const string SignInRewardSign = $"{SgHk4eApi}/event/sol/sign?lang=zh-cn";
|
public const string SignInRewardSign = $"{SgHk4eApi}/event/sol/sign?lang=zh-cn";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 签到
|
|
||||||
/// </summary>
|
|
||||||
public const string SignInRewardHome = $"{SgHk4eApi}/event/sol/home?lang=zh-cn&act_id={SignInRewardActivityId}";
|
public const string SignInRewardHome = $"{SgHk4eApi}/event/sol/home?lang=zh-cn&act_id={SignInRewardActivityId}";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 补签
|
|
||||||
/// </summary>
|
|
||||||
public const string SignInRewardReSign = $"{SgHk4eApi}/event/sol/resign?lang=zh-cn";
|
public const string SignInRewardReSign = $"{SgHk4eApi}/event/sol/resign?lang=zh-cn";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 补签信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>补签信息字符串</returns>
|
|
||||||
public static string SignInRewardResignInfo(in PlayerUid uid)
|
public static string SignInRewardResignInfo(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{SgHk4eApi}/event/sol/resign_info?lang=zh-cn&act_id={SignInRewardActivityId}®ion={uid.Region}&uid={uid.Value}";
|
return $"{SgHk4eApi}/event/sol/resign_info?lang=zh-cn&act_id={SignInRewardActivityId}®ion={uid.Region}&uid={uid.Value}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 签到信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>签到信息字符串</returns>
|
|
||||||
public static string SignInRewardInfo(in PlayerUid uid)
|
public static string SignInRewardInfo(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{SgHk4eApi}/event/sol/info?lang=zh-cn&act_id={SignInRewardActivityId}®ion={uid.Region}&uid={uid.Value}";
|
return $"{SgHk4eApi}/event/sol/info?lang=zh-cn&act_id={SignInRewardActivityId}®ion={uid.Region}&uid={uid.Value}";
|
||||||
@@ -300,7 +124,6 @@ internal static class ApiOsEndpoints
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region SgHoyoPlayApi
|
#region SgHoyoPlayApi
|
||||||
|
|
||||||
public static string HoyoPlayConnectGamePackages(LaunchScheme scheme)
|
public static string HoyoPlayConnectGamePackages(LaunchScheme scheme)
|
||||||
{
|
{
|
||||||
return $"{SgHoyoPlayApiConnectApi}/getGamePackages?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
|
return $"{SgHoyoPlayApiConnectApi}/getGamePackages?game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}";
|
||||||
@@ -315,14 +138,9 @@ internal static class ApiOsEndpoints
|
|||||||
{
|
{
|
||||||
return $"{SgHoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
|
return $"{SgHoyoPlayApiConnectApi}/getGameDeprecatedFileConfigs?channel={scheme.Channel:D}&game_ids[]={scheme.GameId}&launcher_id={scheme.LauncherId}&sub_channel={scheme.SubChannel:D}";
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region WebApiOsAccountApi
|
#region WebApiOsAccountApi
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 使用 Cookie 登录
|
|
||||||
/// </summary>
|
|
||||||
public const string WebApiOsAccountLoginByCookie = $"{WebApiOsAccountApi}/login_by_cookie";
|
public const string WebApiOsAccountLoginByCookie = $"{WebApiOsAccountApi}/login_by_cookie";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -352,25 +170,15 @@ internal static class ApiOsEndpoints
|
|||||||
private const string WebApiOs = "https://webapi-os.account.hoyoverse.com";
|
private const string WebApiOs = "https://webapi-os.account.hoyoverse.com";
|
||||||
private const string WebApiOsAccountApi = $"{WebApiOs}/Api";
|
private const string WebApiOsAccountApi = $"{WebApiOs}/Api";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Web static referer
|
|
||||||
/// </summary>
|
|
||||||
public const string WebStaticSeaMihoyoReferer = "https://webstatic-sea.mihoyo.com";
|
public const string WebStaticSeaMihoyoReferer = "https://webstatic-sea.mihoyo.com";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Act hoyolab referer
|
|
||||||
/// </summary>
|
|
||||||
public const string ActHoyolabReferer = "https://act.hoyolab.com/";
|
public const string ActHoyolabReferer = "https://act.hoyolab.com/";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// App hoyolab referer
|
|
||||||
/// </summary>
|
|
||||||
public const string AppHoyolabReferer = "https://app.hoyolab.com/";
|
public const string AppHoyolabReferer = "https://app.hoyolab.com/";
|
||||||
|
|
||||||
private static string AnnouncementQuery(string languageCode, in Region region)
|
private static string AnnouncementQuery(string languageCode, in Region region)
|
||||||
{
|
{
|
||||||
return $"game=hk4e&game_biz=hk4e_global&lang={languageCode}&bundle_id=hk4e_global&platform=pc®ion={region}&level=55&uid=100000000";
|
return $"game=hk4e&game_biz=hk4e_global&lang={languageCode}&bundle_id=hk4e_global&platform=pc®ion={region}&level=55&uid=100000000";
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ internal sealed partial class PandaClient
|
|||||||
|
|
||||||
public async ValueTask<Response<UrlWrapper>> QRCodeFetchAsync(CancellationToken token = default)
|
public async ValueTask<Response<UrlWrapper>> QRCodeFetchAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40);
|
GameLoginRequest options = GameLoginRequest.Create(8, HoyolabOptions.DeviceId40);
|
||||||
|
|
||||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||||
.SetRequestUri(ApiEndpoints.QrCodeFetch)
|
.SetRequestUri(ApiEndpoints.QrCodeFetch)
|
||||||
@@ -34,10 +34,11 @@ internal sealed partial class PandaClient
|
|||||||
|
|
||||||
public async ValueTask<Response<GameLoginResult>> QRCodeQueryAsync(string ticket, CancellationToken token = default)
|
public async ValueTask<Response<GameLoginResult>> QRCodeQueryAsync(string ticket, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
GameLoginRequest options = GameLoginRequest.Create(4, HoyolabOptions.DeviceId40, ticket);
|
GameLoginRequest options = GameLoginRequest.Create(8, HoyolabOptions.DeviceId40, ticket);
|
||||||
|
|
||||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||||
.SetRequestUri(ApiEndpoints.QrCodeQuery)
|
.SetRequestUri(ApiEndpoints.QrCodeQuery)
|
||||||
|
.SetHeader("x-rpc-device_id", HoyolabOptions.DeviceId40)
|
||||||
.PostJson(options);
|
.PostJson(options);
|
||||||
|
|
||||||
Response<GameLoginResult>? resp = await builder
|
Response<GameLoginResult>? resp = await builder
|
||||||
|
|||||||
@@ -22,4 +22,20 @@ internal sealed class DailyTask
|
|||||||
|
|
||||||
[JsonPropertyName("attendance_visible")]
|
[JsonPropertyName("attendance_visible")]
|
||||||
public bool AttendanceVisible { get; set; }
|
public bool AttendanceVisible { get; set; }
|
||||||
}
|
|
||||||
|
[JsonPropertyName("stored_attendance")]
|
||||||
|
public double StoredAttendance { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("stored_attendance_refresh_countdown")]
|
||||||
|
public int StoredAttendanceRefreshCountdown { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public string StoredAttendanceRefreshCountdownFormat
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
TimeSpan timeSpan = TimeSpan.FromSeconds(StoredAttendanceRefreshCountdown);
|
||||||
|
return SH.FormatWebDailyNoteStoredAttendanceRefreshCountdown(timeSpan);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ internal sealed partial class HutaoInfrastructureClient
|
|||||||
public async ValueTask<HutaoResponse<StaticResourceSizeInformation>> GetStaticSizeAsync(CancellationToken token = default)
|
public async ValueTask<HutaoResponse<StaticResourceSizeInformation>> GetStaticSizeAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||||
.SetRequestUri(HutaoEndpoints.StaticSize)
|
.SetRequestUri(HutaoEndpoints.StaticSize())
|
||||||
.Get();
|
.Get();
|
||||||
|
|
||||||
HutaoResponse<StaticResourceSizeInformation>? resp = await builder.SendAsync<HutaoResponse<StaticResourceSizeInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
HutaoResponse<StaticResourceSizeInformation>? resp = await builder.SendAsync<HutaoResponse<StaticResourceSizeInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
||||||
@@ -32,28 +32,30 @@ internal sealed partial class HutaoInfrastructureClient
|
|||||||
public async ValueTask<HutaoResponse<IPInformation>> GetIPInformationAsync(CancellationToken token = default)
|
public async ValueTask<HutaoResponse<IPInformation>> GetIPInformationAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||||
.SetRequestUri(HutaoEndpoints.Ip)
|
.SetRequestUri(HutaoEndpoints.Ip())
|
||||||
.Get();
|
.Get();
|
||||||
|
|
||||||
HutaoResponse<IPInformation>? resp = await builder.SendAsync<HutaoResponse<IPInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
HutaoResponse<IPInformation>? resp = await builder.SendAsync<HutaoResponse<IPInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
||||||
return Web.Response.Response.DefaultIfNull(resp);
|
return Web.Response.Response.DefaultIfNull(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<HutaoResponse<HutaoVersionInformation>> GetHutaoVersionInfomationAsync(CancellationToken token = default)
|
public async ValueTask<HutaoResponse<HutaoPackageInformation>> GetHutaoVersionInfomationAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
|
string url = HutaoEndpoints.PatchSnapHutao();
|
||||||
|
|
||||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||||
.SetRequestUri(HutaoEndpoints.PatchSnapHutao)
|
.SetRequestUri(url)
|
||||||
.SetHeader("x-hutao-device-id", runtimeOptions.DeviceId)
|
.SetHeader("x-hutao-device-id", runtimeOptions.DeviceId)
|
||||||
.Get();
|
.Get();
|
||||||
|
|
||||||
HutaoResponse<HutaoVersionInformation>? resp = await builder.SendAsync<HutaoResponse<HutaoVersionInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
HutaoResponse<HutaoPackageInformation>? resp = await builder.SendAsync<HutaoResponse<HutaoPackageInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
||||||
return Web.Response.Response.DefaultIfNull(resp);
|
return Web.Response.Response.DefaultIfNull(resp);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask<HutaoResponse<YaeVersionInformation>> GetYaeVersionInformationAsync(CancellationToken token = default)
|
public async ValueTask<HutaoResponse<YaeVersionInformation>> GetYaeVersionInformationAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||||
.SetRequestUri(HutaoEndpoints.PatchYaeAchievement)
|
.SetRequestUri(HutaoEndpoints.PatchYaeAchievement())
|
||||||
.Get();
|
.Get();
|
||||||
|
|
||||||
HutaoResponse<YaeVersionInformation>? resp = await builder.SendAsync<HutaoResponse<YaeVersionInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
HutaoResponse<YaeVersionInformation>? resp = await builder.SendAsync<HutaoResponse<YaeVersionInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Web.Hutao;
|
||||||
|
|
||||||
|
internal sealed class HutaoPackageInformation
|
||||||
|
{
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public Version Version { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("validation")]
|
||||||
|
public string Validation { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("mirrors")]
|
||||||
|
public List<HutaoPackageMirror> Mirrors { get; set; } = default!;
|
||||||
|
}
|
||||||
19
src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPackageMirror.cs
Normal file
19
src/Snap.Hutao/Snap.Hutao/Web/Hutao/HutaoPackageMirror.cs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.Json.Annotation;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Web.Hutao;
|
||||||
|
|
||||||
|
internal sealed class HutaoPackageMirror
|
||||||
|
{
|
||||||
|
[JsonPropertyName("url")]
|
||||||
|
public string Url { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonPropertyName("mirror_name")]
|
||||||
|
public string MirrorName { get; set; } = default!;
|
||||||
|
|
||||||
|
[JsonEnum(JsonSerializeType.String)]
|
||||||
|
[JsonPropertyName("mirror_type")]
|
||||||
|
public HutaoPackageMirrorType MirrorType { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Web.Hutao;
|
||||||
|
|
||||||
|
internal enum HutaoPackageMirrorType
|
||||||
|
{
|
||||||
|
Direct,
|
||||||
|
Archive,
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Web.Hutao;
|
|
||||||
|
|
||||||
internal sealed class HutaoReleaseDescription
|
|
||||||
{
|
|
||||||
[JsonPropertyName("cn")]
|
|
||||||
public string CN { get; set; } = default!;
|
|
||||||
|
|
||||||
[JsonPropertyName("en")]
|
|
||||||
public string EN { get; set; } = default!;
|
|
||||||
|
|
||||||
[JsonPropertyName("full")]
|
|
||||||
public string Full { get; set; } = default!;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
|
||||||
// Licensed under the MIT license.
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Web.Hutao;
|
|
||||||
|
|
||||||
internal sealed class HutaoVersionInformation
|
|
||||||
{
|
|
||||||
[JsonPropertyName("version")]
|
|
||||||
public Version Version { get; set; } = default!;
|
|
||||||
|
|
||||||
[JsonPropertyName("urls")]
|
|
||||||
public List<string> Urls { get; set; } = default!;
|
|
||||||
|
|
||||||
[JsonPropertyName("sha256")]
|
|
||||||
public string? Sha256 { get; set; } = default!;
|
|
||||||
|
|
||||||
[JsonPropertyName("archive_urls")]
|
|
||||||
public List<string> ArchiveUrls { get; set; } = default!;
|
|
||||||
|
|
||||||
[JsonPropertyName("release_description")]
|
|
||||||
public HutaoReleaseDescription ReleaseDescription { get; set; } = default!;
|
|
||||||
}
|
|
||||||
@@ -19,17 +19,17 @@ internal sealed partial class HutaoWallpaperClient
|
|||||||
|
|
||||||
public ValueTask<Response<Wallpaper>> GetBingWallpaperAsync(CancellationToken token = default)
|
public ValueTask<Response<Wallpaper>> GetBingWallpaperAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return GetWallpaperAsync(HutaoEndpoints.WallpaperBing, token);
|
return GetWallpaperAsync(HutaoEndpoints.WallpaperBing(), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Response<Wallpaper>> GetLauncherWallpaperAsync(CancellationToken token = default)
|
public ValueTask<Response<Wallpaper>> GetLauncherWallpaperAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return GetWallpaperAsync(HutaoEndpoints.WallpaperGenshinLauncher, token);
|
return GetWallpaperAsync(HutaoEndpoints.WallpaperGenshinLauncher(), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ValueTask<Response<Wallpaper>> GetTodayWallpaperAsync(CancellationToken token = default)
|
public ValueTask<Response<Wallpaper>> GetTodayWallpaperAsync(CancellationToken token = default)
|
||||||
{
|
{
|
||||||
return GetWallpaperAsync(HutaoEndpoints.WallpaperToday, token);
|
return GetWallpaperAsync(HutaoEndpoints.WallpaperToday(), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ValueTask<Response<Wallpaper>> GetWallpaperAsync(string url, CancellationToken token = default)
|
private async ValueTask<Response<Wallpaper>> GetWallpaperAsync(string url, CancellationToken token = default)
|
||||||
|
|||||||
@@ -3,71 +3,35 @@
|
|||||||
|
|
||||||
using Snap.Hutao.Web.Hoyolab;
|
using Snap.Hutao.Web.Hoyolab;
|
||||||
using Snap.Hutao.Web.Hutao.GachaLog;
|
using Snap.Hutao.Web.Hutao.GachaLog;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Snap.Hutao.Web;
|
namespace Snap.Hutao.Web;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 胡桃 API 端点
|
|
||||||
/// </summary>
|
|
||||||
[HighQuality]
|
|
||||||
[SuppressMessage("", "SA1201")]
|
[SuppressMessage("", "SA1201")]
|
||||||
[SuppressMessage("", "SA1203")]
|
[SuppressMessage("", "SA1203")]
|
||||||
internal static class HutaoEndpoints
|
internal static partial class HutaoEndpoints
|
||||||
{
|
{
|
||||||
#region HomaAPI
|
#region HomaAPI
|
||||||
|
|
||||||
#region GachaLog
|
#region GachaLog
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取末尾Id
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>获取末尾Id Url</returns>
|
|
||||||
public static string GachaLogEndIds(string uid)
|
public static string GachaLogEndIds(string uid)
|
||||||
{
|
{
|
||||||
return $"{HomaSnapGenshin}/GachaLog/EndIds?Uid={uid}";
|
return $"{HomaSnapGenshin}/GachaLog/EndIds?Uid={uid}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取祈愿记录
|
|
||||||
/// </summary>
|
|
||||||
public const string GachaLogRetrieve = $"{HomaSnapGenshin}/GachaLog/Retrieve";
|
public const string GachaLogRetrieve = $"{HomaSnapGenshin}/GachaLog/Retrieve";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 上传祈愿记录
|
|
||||||
/// </summary>
|
|
||||||
public const string GachaLogUpload = $"{HomaSnapGenshin}/GachaLog/Upload";
|
public const string GachaLogUpload = $"{HomaSnapGenshin}/GachaLog/Upload";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取Uid列表
|
|
||||||
/// </summary>
|
|
||||||
public const string GachaLogUids = $"{HomaSnapGenshin}/GachaLog/Uids";
|
public const string GachaLogUids = $"{HomaSnapGenshin}/GachaLog/Uids";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取Uid列表
|
|
||||||
/// </summary>
|
|
||||||
public const string GachaLogEntries = $"{HomaSnapGenshin}/GachaLog/Entries";
|
public const string GachaLogEntries = $"{HomaSnapGenshin}/GachaLog/Entries";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 删除祈愿记录
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>删除祈愿记录 Url</returns>
|
|
||||||
public static string GachaLogDelete(string uid)
|
public static string GachaLogDelete(string uid)
|
||||||
{
|
{
|
||||||
return $"{HomaSnapGenshin}/GachaLog/Delete?Uid={uid}";
|
return $"{HomaSnapGenshin}/GachaLog/Delete?Uid={uid}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取祈愿统计信息
|
|
||||||
/// </summary>
|
|
||||||
public const string GachaLogStatisticsCurrentEvents = $"{HomaSnapGenshin}/GachaLog/Statistics/CurrentEventStatistics";
|
public const string GachaLogStatisticsCurrentEvents = $"{HomaSnapGenshin}/GachaLog/Statistics/CurrentEventStatistics";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取祈愿统计信息
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="distributionType">分布类型</param>
|
|
||||||
/// <returns>祈愿统计信息Url</returns>
|
|
||||||
public static string GachaLogStatisticsDistribution(GachaDistributionType distributionType)
|
public static string GachaLogStatisticsDistribution(GachaDistributionType distributionType)
|
||||||
{
|
{
|
||||||
return $"{HomaSnapGenshin}/GachaLog/Statistics/Distribution/{distributionType}";
|
return $"{HomaSnapGenshin}/GachaLog/Statistics/Distribution/{distributionType}";
|
||||||
@@ -94,71 +58,29 @@ internal static class HutaoEndpoints
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region LogUpload
|
#region LogUpload
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 上传日志
|
|
||||||
/// </summary>
|
|
||||||
public const string HutaoLogUpload = $"{HomaSnapGenshin}/HutaoLog/Upload";
|
public const string HutaoLogUpload = $"{HomaSnapGenshin}/HutaoLog/Upload";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Passport
|
#region Passport
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 获取注册验证码
|
|
||||||
/// </summary>
|
|
||||||
public const string PassportVerify = $"{HomaSnapGenshin}/Passport/Verify";
|
public const string PassportVerify = $"{HomaSnapGenshin}/Passport/Verify";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注册账号
|
|
||||||
/// </summary>
|
|
||||||
public const string PassportRegister = $"{HomaSnapGenshin}/Passport/Register";
|
public const string PassportRegister = $"{HomaSnapGenshin}/Passport/Register";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 注销账号
|
|
||||||
/// </summary>
|
|
||||||
public const string PassportCancel = $"{HomaSnapGenshin}/Passport/Cancel";
|
public const string PassportCancel = $"{HomaSnapGenshin}/Passport/Cancel";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 重设密码
|
|
||||||
/// </summary>
|
|
||||||
public const string PassportResetPassword = $"{HomaSnapGenshin}/Passport/ResetPassword";
|
public const string PassportResetPassword = $"{HomaSnapGenshin}/Passport/ResetPassword";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 登录
|
|
||||||
/// </summary>
|
|
||||||
public const string PassportLogin = $"{HomaSnapGenshin}/Passport/Login";
|
public const string PassportLogin = $"{HomaSnapGenshin}/Passport/Login";
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 用户信息
|
|
||||||
/// </summary>
|
|
||||||
public const string PassportUserInfo = $"{HomaSnapGenshin}/Passport/UserInfo";
|
public const string PassportUserInfo = $"{HomaSnapGenshin}/Passport/UserInfo";
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region SpiralAbyss
|
#region SpiralAbyss
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 检查 uid 是否上传记录
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>路径</returns>
|
|
||||||
public static string RecordCheck(string uid)
|
public static string RecordCheck(string uid)
|
||||||
{
|
{
|
||||||
return $"{HomaSnapGenshin}/Record/Check?uid={uid}";
|
return $"{HomaSnapGenshin}/Record/Check?uid={uid}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// uid 排行
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="uid">uid</param>
|
|
||||||
/// <returns>路径</returns>
|
|
||||||
public static string RecordRank(string uid)
|
public static string RecordRank(string uid)
|
||||||
{
|
{
|
||||||
return $"{HomaSnapGenshin}/Record/Rank?uid={uid}";
|
return $"{HomaSnapGenshin}/Record/Rank?uid={uid}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 上传记录
|
|
||||||
/// </summary>
|
|
||||||
public const string RecordUpload = $"{HomaSnapGenshin}/Record/Upload";
|
public const string RecordUpload = $"{HomaSnapGenshin}/Record/Upload";
|
||||||
|
|
||||||
public const string StatisticsOverview = $"{HomaSnapGenshin}/Statistics/Overview";
|
public const string StatisticsOverview = $"{HomaSnapGenshin}/Statistics/Overview";
|
||||||
@@ -181,99 +103,166 @@ internal static class HutaoEndpoints
|
|||||||
{
|
{
|
||||||
return $"{HomaSnapGenshin}/{path}";
|
return $"{HomaSnapGenshin}/{path}";
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Infrasturcture
|
#region Infrasturcture
|
||||||
|
|
||||||
public static string Enka(in PlayerUid uid)
|
public static string Enka(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{ApiSnapGenshinEnka}/{uid}";
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/enka/{uid}",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/enka/{uid}",
|
||||||
|
_ => $"{ApiSnapGenshin}/enka/{uid}",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string EnkaPlayerInfo(in PlayerUid uid)
|
public static string EnkaPlayerInfo(in PlayerUid uid)
|
||||||
{
|
{
|
||||||
return $"{ApiSnapGenshinEnka}/{uid}/info";
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/enka/{uid}/info",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/enka/{uid}/info",
|
||||||
|
_ => $"{ApiSnapGenshin}/enka/{uid}/info",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string Ip = $"{ApiSnapGenshin}/ip";
|
public static string Ip()
|
||||||
|
{
|
||||||
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/ip",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/ip",
|
||||||
|
_ => $"{ApiSnapGenshin}/ip",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Feature
|
||||||
|
public static string Feature(string name)
|
||||||
|
{
|
||||||
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/client/{name}.json",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/client/{name}.json",
|
||||||
|
_ => $"{ApiSnapGenshin}/client/{name}.json",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Metadata
|
#region Metadata
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 胡桃元数据2文件
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="locale">语言</param>
|
|
||||||
/// <param name="fileName">文件名称</param>
|
|
||||||
/// <returns>路径</returns>
|
|
||||||
public static string Metadata(string locale, string fileName)
|
public static string Metadata(string locale, string fileName)
|
||||||
{
|
{
|
||||||
return $"{ApiSnapGenshinMetadata}/Genshin/{locale}/{fileName}";
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/metadata/Genshin/{locale}/{fileName}",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/metadata/Genshin/{locale}/{fileName}",
|
||||||
|
_ => $"{ApiSnapGenshin}/metadata/Genshin/{locale}/{fileName}",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Patch
|
#region Patch
|
||||||
public const string PatchYaeAchievement = $"{ApiSnapGenshinPatch}/yae";
|
public static string PatchYaeAchievement()
|
||||||
public const string PatchSnapHutao = $"{ApiSnapGenshinPatch}/hutao";
|
{
|
||||||
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/patch/yae",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/patch/yae",
|
||||||
|
_ => $"{ApiSnapGenshin}/patch/yae",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string PatchSnapHutao()
|
||||||
|
{
|
||||||
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/patch/hutao",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/patch/hutao",
|
||||||
|
_ => $"{ApiSnapGenshin}/patch/hutao",
|
||||||
|
};
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region StaticResources
|
#region StaticResources
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI_Icon_None
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Uri UIIconNone = StaticRaw("Bg", "UI_Icon_None.png").ToUri();
|
public static readonly Uri UIIconNone = StaticRaw("Bg", "UI_Icon_None.png").ToUri();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI_ItemIcon_None
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Uri UIItemIconNone = StaticRaw("Bg", "UI_ItemIcon_None.png").ToUri();
|
public static readonly Uri UIItemIconNone = StaticRaw("Bg", "UI_ItemIcon_None.png").ToUri();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI_AvatarIcon_Side_None
|
|
||||||
/// </summary>
|
|
||||||
public static readonly Uri UIAvatarIconSideNone = StaticRaw("AvatarIcon", "UI_AvatarIcon_Side_None.png").ToUri();
|
public static readonly Uri UIAvatarIconSideNone = StaticRaw("AvatarIcon", "UI_AvatarIcon_Side_None.png").ToUri();
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 图片资源
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="category">分类</param>
|
|
||||||
/// <param name="fileName">文件名称 包括后缀</param>
|
|
||||||
/// <returns>路径</returns>
|
|
||||||
public static string StaticRaw(string category, string fileName)
|
public static string StaticRaw(string category, string fileName)
|
||||||
{
|
{
|
||||||
return $"{ApiSnapGenshinStaticRaw}/{category}/{fileName}";
|
return $"{ApiSnapGenshin}/static/raw/{category}/{fileName}";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 压缩包资源
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="fileName">文件名称 不包括后缀</param>
|
|
||||||
/// <returns>路径</returns>
|
|
||||||
public static string StaticZip(string fileName)
|
public static string StaticZip(string fileName)
|
||||||
{
|
{
|
||||||
return $"{ApiSnapGenshinStaticZip}/{fileName}.zip";
|
return $"{ApiSnapGenshin}/static/zip/{fileName}.zip";
|
||||||
}
|
}
|
||||||
|
|
||||||
public const string StaticSize = $"{ApiSnapGenshin}/static/size";
|
public static string StaticSize()
|
||||||
|
{
|
||||||
|
return $"{ApiSnapGenshin}/static/size";
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Wallpaper
|
#region Wallpaper
|
||||||
|
|
||||||
public const string WallpaperBing = $"{ApiSnapGenshin}/wallpaper/bing";
|
public static string WallpaperBing()
|
||||||
|
{
|
||||||
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/wallpaper/bing",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/wallpaper/bing",
|
||||||
|
_ => $"{ApiSnapGenshin}/wallpaper/bing",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public const string WallpaperGenshinLauncher = $"{ApiSnapGenshin}/wallpaper/hoyoplay";
|
public static string WallpaperGenshinLauncher()
|
||||||
|
{
|
||||||
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/wallpaper/genshinlauncher",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/wallpaper/genshinlauncher",
|
||||||
|
_ => $"{ApiSnapGenshin}/wallpaper/genshinlauncher",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public const string WallpaperToday = $"{ApiSnapGenshin}/wallpaper/today";
|
public static string WallpaperToday()
|
||||||
|
{
|
||||||
|
return Kind switch
|
||||||
|
{
|
||||||
|
ApiKind.AlphaCN => $"{ApiAlphaSnapGenshin}/cn/wallpaper/today",
|
||||||
|
ApiKind.AlphaOS => $"{ApiAlphaSnapGenshin}/global/wallpaper/today",
|
||||||
|
_ => $"{ApiSnapGenshin}/wallpaper/today",
|
||||||
|
};
|
||||||
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
private const string ApiSnapGenshin = "https://api.snapgenshin.com";
|
private const string ApiSnapGenshin = "https://api.snapgenshin.com";
|
||||||
private const string ApiSnapGenshinMetadata = $"{ApiSnapGenshin}/metadata";
|
|
||||||
private const string ApiSnapGenshinPatch = $"{ApiSnapGenshin}/patch";
|
|
||||||
private const string ApiSnapGenshinStaticRaw = $"{ApiSnapGenshin}/static/raw";
|
|
||||||
private const string ApiSnapGenshinStaticZip = $"{ApiSnapGenshin}/static/zip";
|
|
||||||
private const string ApiSnapGenshinEnka = $"{ApiSnapGenshin}/enka";
|
|
||||||
private const string HomaSnapGenshin = "https://homa.snapgenshin.com";
|
private const string HomaSnapGenshin = "https://homa.snapgenshin.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static partial class HutaoEndpoints
|
||||||
|
{
|
||||||
|
private enum ApiKind
|
||||||
|
{
|
||||||
|
AlphaCN,
|
||||||
|
AlphaOS,
|
||||||
|
Formal,
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ApiKind Kind
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
get
|
||||||
|
{
|
||||||
|
#if IS_ALPHA_BUILD || DEBUG
|
||||||
|
return Core.Setting.LocalSetting.Get(Core.Setting.SettingKeys.AlphaBuildUseCNPatchEndpoint, false) ? ApiKind.AlphaCN : ApiKind.AlphaOS;
|
||||||
|
#else
|
||||||
|
return ApiKind.Formal;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string ApiAlphaSnapGenshin = "https://api-alpha.snapgenshin.cn";
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core;
|
||||||
using Snap.Hutao.Service.Notification;
|
using Snap.Hutao.Service.Notification;
|
||||||
using Snap.Hutao.Web.Hutao.Response;
|
using Snap.Hutao.Web.Hutao.Response;
|
||||||
using Snap.Hutao.Web.Response;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -45,6 +45,7 @@ internal static class HttpRequestMessageBuilderExtension
|
|||||||
{
|
{
|
||||||
if (TryHandleHttp502HutaoResponseSpecialCase(ex, out TResult? result))
|
if (TryHandleHttp502HutaoResponseSpecialCase(ex, out TResult? result))
|
||||||
{
|
{
|
||||||
|
showInfo = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +72,16 @@ internal static class HttpRequestMessageBuilderExtension
|
|||||||
ProcessException(messageBuilder, ex);
|
ProcessException(messageBuilder, ex);
|
||||||
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||||
}
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
showInfo = false;
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
ProcessException(messageBuilder, ex);
|
||||||
|
logger.LogWarning(ex, RequestErrorMessage, builder.RequestUri);
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (showInfo)
|
if (showInfo)
|
||||||
@@ -148,39 +159,43 @@ internal static class HttpRequestMessageBuilderExtension
|
|||||||
.AppendLine($"{nameof(HttpRequestException)}: Status Code: {hre.StatusCode} Error: {hre.HttpRequestError}")
|
.AppendLine($"{nameof(HttpRequestException)}: Status Code: {hre.StatusCode} Error: {hre.HttpRequestError}")
|
||||||
.AppendLine(hre.Message);
|
.AppendLine(hre.Message);
|
||||||
}
|
}
|
||||||
|
else if (exception is IOException ioe)
|
||||||
if (exception is IOException ioe)
|
|
||||||
{
|
{
|
||||||
builder
|
builder
|
||||||
.AppendLine($"{nameof(IOException)}: 0x{ioe.HResult:X8}")
|
.AppendLine($"{nameof(IOException)}: 0x{ioe.HResult:X8}")
|
||||||
.AppendLine(ioe.Message);
|
.AppendLine(ioe.Message);
|
||||||
}
|
}
|
||||||
|
else if (exception is JsonException je)
|
||||||
if (exception is JsonException je)
|
|
||||||
{
|
{
|
||||||
builder
|
builder
|
||||||
.AppendLine($"{nameof(JsonException)}: Path: {je.Path} at Line: {je.LineNumber} Position: {je.BytePositionInLine}")
|
.AppendLine($"{nameof(JsonException)}: Path: {je.Path} at Line: {je.LineNumber} Position: {je.BytePositionInLine}")
|
||||||
.AppendLine(je.Message);
|
.AppendLine(je.Message);
|
||||||
}
|
}
|
||||||
|
else if (exception is HttpContentSerializationException hcse)
|
||||||
if (exception is HttpContentSerializationException hcse)
|
|
||||||
{
|
{
|
||||||
builder
|
builder
|
||||||
.AppendLine($"{nameof(HttpContentSerializationException)}:")
|
.AppendLine($"{nameof(HttpContentSerializationException)}:")
|
||||||
.AppendLine(hcse.Message);
|
.AppendLine(hcse.Message);
|
||||||
}
|
}
|
||||||
|
else if (exception is SocketException se)
|
||||||
if (exception is SocketException se)
|
|
||||||
{
|
{
|
||||||
builder
|
builder
|
||||||
.AppendLine($"{nameof(SocketException)}: Error: {se.SocketErrorCode}")
|
.AppendLine($"{nameof(SocketException)}: Error: {se.SocketErrorCode}")
|
||||||
.AppendLine(se.Message);
|
.AppendLine(se.Message);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.AppendLine($"{TypeNameHelper.GetTypeDisplayName(exception, false)}:")
|
||||||
|
.AppendLine(exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
if (exception.InnerException is { } inner)
|
if (exception.InnerException is { } inner)
|
||||||
{
|
{
|
||||||
builder.AppendLine(new string('-', 40));
|
builder.AppendLine("------------------ Inner Exception ------------------");
|
||||||
ProcessException(builder, inner);
|
ProcessException(builder, inner);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder.AppendLine("------------------ End ------------------");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,11 @@ internal enum KnownReturnCode
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
AlreadySignedIn = -5003,
|
AlreadySignedIn = -5003,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 请求失败,当前设备或网络环境存在风险
|
||||||
|
/// </summary>
|
||||||
|
CODEN3503 = -3503,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 需要风险验证(闪验)
|
/// 需要风险验证(闪验)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ using System.Diagnostics;
|
|||||||
|
|
||||||
namespace Snap.Hutao.Web.WebView2;
|
namespace Snap.Hutao.Web.WebView2;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Bridge 拓展
|
|
||||||
/// </summary>
|
|
||||||
[HighQuality]
|
|
||||||
internal static class WebView2Extension
|
internal static class WebView2Extension
|
||||||
{
|
{
|
||||||
[Conditional("RELEASE")]
|
[Conditional("RELEASE")]
|
||||||
|
|||||||
Reference in New Issue
Block a user