mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
3 Commits
develop
...
advanced-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c261b7866 | ||
|
|
fc2d590c42 | ||
|
|
45724801ee |
@@ -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; }
|
||||
}
|
||||
}
|
||||
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 LaunchIsScreenHeightEnabled = "Launch.IsScreenHeightEnabled";
|
||||
public const string LaunchUnlockFps = "Launch.UnlockFps";
|
||||
public const string LaunchUnlockerKind = "Launch.UnlockerKind";
|
||||
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 LaunchIsMonitorEnabled = "Launch.IsMonitorEnabled";
|
||||
public const string LaunchIsUseCloudThirdPartyMobile = "Launch.IsUseCloudThirdPartyMobile";
|
||||
@@ -51,6 +52,9 @@ internal sealed partial class SettingEntry
|
||||
public const string LaunchUseBetterGenshinImpactAutomation = "Launch.UseBetterGenshinImpactAutomation";
|
||||
public const string LaunchSetDiscordActivityWhenPlaying = "Launch.SetDiscordActivityWhenPlaying";
|
||||
|
||||
[Obsolete("不再区分解锁器类型,统一使用注入")]
|
||||
public const string LaunchUnlockerKind = "Launch.UnlockerKind";
|
||||
|
||||
[Obsolete("不再支持多开")]
|
||||
public const string MultipleInstances = "Launch.MultipleInstances";
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ internal sealed class Hk4eItem : IMappingFrom<Hk4eItem, GachaItem>
|
||||
public required GachaType GachaType { get; set; }
|
||||
|
||||
[JsonPropertyName("item_id")]
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)]
|
||||
public required uint ItemId { get; set; }
|
||||
|
||||
[JsonPropertyName("time")]
|
||||
|
||||
@@ -2441,6 +2441,18 @@
|
||||
<data name="ViewPageLaunchGameConfigurationSaveHint" xml:space="preserve">
|
||||
<value>所有选项仅会在启动游戏成功后保存</value>
|
||||
</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">
|
||||
<value>在我游戏时设置 Discord Activity 状态</value>
|
||||
</data>
|
||||
@@ -2525,8 +2537,14 @@
|
||||
<data name="ViewPageLaunchGameSwitchSchemeWarning" xml:space="preserve">
|
||||
<value>版本更新前需要提前转换至与启动器匹配的服务器</value>
|
||||
</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">
|
||||
<value>请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率</value>
|
||||
<value>请在游戏内关闭「垂直同步」选项,需要高性能的显卡以支持更高的帧率,将值设置为 -1 以表示无限制帧率</value>
|
||||
</data>
|
||||
<data name="ViewPageLaunchGameUnlockFpsHeader" xml:space="preserve">
|
||||
<value>解锁帧率限制</value>
|
||||
|
||||
@@ -32,7 +32,7 @@ internal abstract partial class DbStoreOptions : ObservableObject
|
||||
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())
|
||||
{
|
||||
@@ -109,6 +109,28 @@ internal abstract partial class DbStoreOptions : ObservableObject
|
||||
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))]
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
return SetOption(ref storage, key, value, v => v, propertyName);
|
||||
}
|
||||
|
||||
protected bool SetOption(ref bool? storage, string key, bool value, [CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
bool set = SetProperty(ref storage, value, 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;
|
||||
return SetOption(ref storage, key, value, v => $"{v}", propertyName);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
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 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;
|
||||
return false;
|
||||
}
|
||||
|
||||
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.AddAndSave(new(key, serializer(value)));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
using Snap.Hutao.Service.Game.Unlocker;
|
||||
using Snap.Hutao.Win32.Graphics.Gdi;
|
||||
using System.Collections.Immutable;
|
||||
using System.Globalization;
|
||||
@@ -39,8 +38,9 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
private int? screenHeight;
|
||||
private bool? isScreenHeightEnabled;
|
||||
private bool? unlockFps;
|
||||
private NameDescriptionValue<GameFpsUnlockerKind>? unlockerKind;
|
||||
private int? targetFps;
|
||||
private float? targetFov;
|
||||
private bool? disableFog;
|
||||
private NameValue<int>? monitor;
|
||||
private bool? isMonitorEnabled;
|
||||
private bool? isUseCloudThirdPartyMobile;
|
||||
@@ -60,6 +60,63 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
InitializeMonitors(Monitors);
|
||||
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)
|
||||
{
|
||||
try
|
||||
@@ -109,21 +166,22 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
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
|
||||
{
|
||||
get => GetOption(ref isAdvancedLaunchOptionsEnabled, SettingEntry.IsAdvancedLaunchOptionsEnabled);
|
||||
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
|
||||
{
|
||||
get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen);
|
||||
get => GetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen, false);
|
||||
set => SetOption(ref isFullScreen, SettingEntry.LaunchIsFullScreen, value);
|
||||
}
|
||||
|
||||
@@ -169,44 +227,25 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
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
|
||||
{
|
||||
get => GetOption(ref targetFps, SettingEntry.LaunchTargetFps, primaryScreenFps);
|
||||
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 NameValue<int> Monitor
|
||||
public bool DisableFog
|
||||
{
|
||||
get => GetOption(ref disableFog, SettingEntry.LaunchDisableFog, false);
|
||||
set => SetOption(ref disableFog, SettingEntry.LaunchDisableFog, value);
|
||||
}
|
||||
|
||||
public NameValue<int>? Monitor
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -245,6 +284,27 @@ internal sealed class LaunchOptions : DbStoreOptions
|
||||
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; } =
|
||||
[
|
||||
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)
|
||||
{
|
||||
if (unlockerState.FindModuleResult == FindModuleResult.Ok)
|
||||
{
|
||||
return new(LaunchPhase.UnlockFpsSucceed, unlockerState.Description ?? SH.ServiceGameLaunchPhaseUnlockFpsSucceed);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(LaunchPhase.UnlockFpsFailed, unlockerState.Description ?? SH.ServiceGameLaunchPhaseUnlockFpsFailed);
|
||||
}
|
||||
return new(LaunchPhase.UnlockFpsSucceed, unlockerState.Description ?? SH.ServiceGameLaunchPhaseUnlockFpsSucceed);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Factory.Progress;
|
||||
using Snap.Hutao.Service.Game.Unlocker;
|
||||
using Snap.Hutao.Service.Game.Unlocker.Island;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Launching.Handler;
|
||||
|
||||
@@ -25,12 +24,7 @@ internal sealed class LaunchExecutionUnlockFpsHandler : ILaunchExecutionDelegate
|
||||
return;
|
||||
}
|
||||
|
||||
UnlockOptions unlockOptions = new(gameFileSystem, 100, 20000, 2000);
|
||||
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),
|
||||
};
|
||||
GameFpsUnlocker unlocker = new(context.ServiceProvider, context.Process, progress);
|
||||
|
||||
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,66 +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; // advanced to [call jumpCall]
|
||||
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
|
||||
// mov ecx, 60 : B9 3C 00 00 00
|
||||
// call ...
|
||||
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.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
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.System.LibraryLoader;
|
||||
using Snap.Hutao.Win32.System.Threading;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
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.Macros;
|
||||
using static Snap.Hutao.Win32.User32;
|
||||
|
||||
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 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>();
|
||||
featureService = serviceProvider.GetRequiredService<IFeatureService>();
|
||||
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
dataFolderIslandPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.UnlockerIsland.dll");
|
||||
|
||||
context.GameProcess = gameProcess;
|
||||
context.AllAccess = OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_ALL_ACCESS, false, (uint)gameProcess.Id);
|
||||
context.Options = options;
|
||||
context.Progress = progress;
|
||||
context.Logger = serviceProvider.GetRequiredService<ILogger<GameFpsUnlocker>>();
|
||||
}
|
||||
@@ -29,31 +45,114 @@ internal abstract class GameFpsUnlocker : IGameFpsUnlocker
|
||||
public async ValueTask<bool> UnlockAsync(CancellationToken token = default)
|
||||
{
|
||||
HutaoException.ThrowIfNot(context.IsUnlockerValid, "This Unlocker is invalid");
|
||||
(FindModuleResult result, RequiredRemoteModule remoteModule) = await GameProcessModule.FindModuleAsync(context).ConfigureAwait(false);
|
||||
HutaoException.ThrowIfNot(result != FindModuleResult.TimeLimitExeeded, SH.ServiceGameUnlockerFindModuleTimeLimitExeeded);
|
||||
HutaoException.ThrowIfNot(result != FindModuleResult.NoModuleFound, SH.ServiceGameUnlockerFindModuleNoModuleFound);
|
||||
|
||||
using (RequiredLocalModule localModule = LoadRequiredLocalModule(context.Options.GameFileSystem))
|
||||
if (await featureService.GetIslandFeatureAsync().ConfigureAwait(false) is not { } feature)
|
||||
{
|
||||
GameFpsAddress.UnsafeFindFpsAddress(context, remoteModule, localModule);
|
||||
return false;
|
||||
}
|
||||
|
||||
offsets = string.Equals(GameConstants.GenshinImpactProcessName, context.GameProcess.ProcessName, StringComparison.OrdinalIgnoreCase)
|
||||
? feature.Oversea
|
||||
: feature.Chinese;
|
||||
|
||||
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 RequiredLocalModule LoadRequiredLocalModule(GameFileSystem gameFileSystem)
|
||||
private static unsafe void InitializeIslandEnvironment(nint handle, IslandFunctionOffsets offsets, LaunchOptions options)
|
||||
{
|
||||
LOAD_LIBRARY_FLAGS flags = LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_IMAGE_RESOURCE;
|
||||
HMODULE executaleAddress = LoadLibraryExW(gameFileSystem.GameFilePath, default, flags);
|
||||
IslandEnvironment* pIslandEnvironment = (IslandEnvironment*)handle;
|
||||
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.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace Snap.Hutao.Service.Game.Unlocker;
|
||||
@@ -10,18 +9,10 @@ internal sealed class GameFpsUnlockerContext
|
||||
{
|
||||
public string Description { get; set; } = default!;
|
||||
|
||||
public FindModuleResult FindModuleResult { get; set; }
|
||||
|
||||
public bool IsUnlockerValid { get; set; } = true;
|
||||
|
||||
public nuint FpsAddress { get; set; }
|
||||
|
||||
public UnlockOptions Options { get; set; }
|
||||
|
||||
public Process GameProcess { get; set; } = default!;
|
||||
|
||||
public HANDLE AllAccess { get; set; }
|
||||
|
||||
public IProgress<GameFpsUnlockerContext> Progress { 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
|
||||
{
|
||||
public nuint Address;
|
||||
public int Value;
|
||||
public nuint Reserved1;
|
||||
public int Reserved2;
|
||||
public IslandState State;
|
||||
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
|
||||
{
|
||||
public nuint Address;
|
||||
public int Value;
|
||||
public nuint Reserved1;
|
||||
public int Reserved2;
|
||||
public IslandState State;
|
||||
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,120 +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 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))
|
||||
{
|
||||
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();
|
||||
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 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);
|
||||
}
|
||||
}
|
||||
@@ -320,7 +320,7 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Snap.Hutao.UnlockerIsland" Version="1.0.10">
|
||||
<PackageReference Include="Snap.Hutao.UnlockerIsland" Version="1.1.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -16,7 +16,7 @@ internal sealed class Int32ToVisibilityConverter : IValueConverter
|
||||
/// <inheritdoc/>
|
||||
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/>
|
||||
|
||||
@@ -357,7 +357,7 @@
|
||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
||||
Padding="10,8,0,0"
|
||||
Maximum="720"
|
||||
Minimum="60"
|
||||
Minimum="-1"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{Binding LaunchOptions.TargetFps, Mode=TwoWay}"/>
|
||||
<ToggleSwitch
|
||||
@@ -367,23 +367,22 @@
|
||||
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsOn}"/>
|
||||
</StackPanel>
|
||||
<cwc:SettingsExpander.Items>
|
||||
<cwc:SettingsCard Description="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsKindDescription}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameUnlockFpsKindHeader}">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="3">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
Foreground="{ThemeResource SystemControlErrorTextForegroundBrush}"
|
||||
Opacity="0.8"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{Binding LaunchOptions.UnlockerKind.Description, Mode=OneWay}"/>
|
||||
<shuxc:SizeRestrictedContentControl HorizontalAlignment="Right">
|
||||
<ComboBox
|
||||
DisplayMemberPath="Name"
|
||||
ItemsSource="{Binding LaunchOptions.UnlockerKinds, Mode=OneWay}"
|
||||
SelectedItem="{Binding LaunchOptions.UnlockerKind, Mode=TwoWay}"/>
|
||||
</shuxc:SizeRestrictedContentControl>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<cwc:SettingsCard Description="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovDescription}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameTargetFovHeader}">
|
||||
<NumberBox
|
||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth2}"
|
||||
Padding="10,8,0,0"
|
||||
Maximum="55"
|
||||
Minimum="45"
|
||||
SmallChange="1"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{Binding LaunchOptions.TargetFov, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
<cwc:SettingsCard Description="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogDescription}" Header="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogHeader}">
|
||||
<ToggleSwitch
|
||||
MinWidth="{ThemeResource SettingsCardContentControlMinWidth}"
|
||||
IsOn="{Binding LaunchOptions.DisableFog, Mode=TwoWay}"
|
||||
OffContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOff}"
|
||||
OnContent="{shuxm:ResourceString Name=ViewPageLaunchGameDisableFogOn}"/>
|
||||
</cwc:SettingsCard>
|
||||
</cwc:SettingsExpander.Items>
|
||||
</cwc:SettingsExpander>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
#define IS_ALPHA_BUILD
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
|
||||
using Snap.Hutao.Web.Hutao.Response;
|
||||
@@ -24,7 +22,7 @@ internal sealed partial class HutaoInfrastructureClient
|
||||
public async ValueTask<HutaoResponse<StaticResourceSizeInformation>> GetStaticSizeAsync(CancellationToken token = default)
|
||||
{
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(HutaoEndpoints.StaticSize)
|
||||
.SetRequestUri(HutaoEndpoints.StaticSize())
|
||||
.Get();
|
||||
|
||||
HutaoResponse<StaticResourceSizeInformation>? resp = await builder.SendAsync<HutaoResponse<StaticResourceSizeInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
||||
@@ -34,7 +32,7 @@ internal sealed partial class HutaoInfrastructureClient
|
||||
public async ValueTask<HutaoResponse<IPInformation>> GetIPInformationAsync(CancellationToken token = default)
|
||||
{
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(HutaoEndpoints.Ip)
|
||||
.SetRequestUri(HutaoEndpoints.Ip())
|
||||
.Get();
|
||||
|
||||
HutaoResponse<IPInformation>? resp = await builder.SendAsync<HutaoResponse<IPInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
||||
@@ -43,12 +41,7 @@ internal sealed partial class HutaoInfrastructureClient
|
||||
|
||||
public async ValueTask<HutaoResponse<HutaoPackageInformation>> GetHutaoVersionInfomationAsync(CancellationToken token = default)
|
||||
{
|
||||
string url
|
||||
#if IS_ALPHA_BUILD
|
||||
= HutaoEndpoints.PatchAlphaSnapHutao(Core.Setting.LocalSetting.Get(Core.Setting.SettingKeys.AlphaBuildUseCNPatchEndpoint, false));
|
||||
#else
|
||||
= HutaoEndpoints.PatchSnapHutao;
|
||||
#endif
|
||||
string url = HutaoEndpoints.PatchSnapHutao();
|
||||
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(url)
|
||||
@@ -62,7 +55,7 @@ internal sealed partial class HutaoInfrastructureClient
|
||||
public async ValueTask<HutaoResponse<YaeVersionInformation>> GetYaeVersionInformationAsync(CancellationToken token = default)
|
||||
{
|
||||
HttpRequestMessageBuilder builder = httpRequestMessageBuilderFactory.Create()
|
||||
.SetRequestUri(HutaoEndpoints.PatchYaeAchievement)
|
||||
.SetRequestUri(HutaoEndpoints.PatchYaeAchievement())
|
||||
.Get();
|
||||
|
||||
HutaoResponse<YaeVersionInformation>? resp = await builder.SendAsync<HutaoResponse<YaeVersionInformation>>(httpClient, logger, token).ConfigureAwait(false);
|
||||
|
||||
@@ -19,17 +19,17 @@ internal sealed partial class HutaoWallpaperClient
|
||||
|
||||
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)
|
||||
{
|
||||
return GetWallpaperAsync(HutaoEndpoints.WallpaperGenshinLauncher, token);
|
||||
return GetWallpaperAsync(HutaoEndpoints.WallpaperGenshinLauncher(), token);
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hutao.GachaLog;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.Web;
|
||||
|
||||
[SuppressMessage("", "SA1201")]
|
||||
[SuppressMessage("", "SA1203")]
|
||||
internal static class HutaoEndpoints
|
||||
internal static partial class HutaoEndpoints
|
||||
{
|
||||
#region HomaAPI
|
||||
|
||||
@@ -107,32 +108,77 @@ internal static class HutaoEndpoints
|
||||
#region Infrasturcture
|
||||
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)
|
||||
{
|
||||
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
|
||||
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
|
||||
|
||||
#region Patch
|
||||
public const string PatchYaeAchievement = $"{ApiSnapGenshinPatch}/yae";
|
||||
public const string PatchSnapHutao = $"{ApiSnapGenshinPatch}/hutao";
|
||||
|
||||
public static string PatchAlphaSnapHutao(bool isCN)
|
||||
public static string PatchYaeAchievement()
|
||||
{
|
||||
return isCN
|
||||
? $"{ApiAlphaSnapGenshin}/cn/patch/hutao"
|
||||
: $"{ApiAlphaSnapGenshin}/global/patch/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
|
||||
|
||||
@@ -143,34 +189,80 @@ internal static class HutaoEndpoints
|
||||
|
||||
public static string StaticRaw(string category, string fileName)
|
||||
{
|
||||
return $"{ApiSnapGenshinStaticRaw}/{category}/{fileName}";
|
||||
return $"{ApiSnapGenshin}/static/raw/{category}/{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
|
||||
|
||||
#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
|
||||
|
||||
private const string ApiSnapGenshin = "https://api.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";
|
||||
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";
|
||||
}
|
||||
Reference in New Issue
Block a user