mirror of
https://jihulab.com/DGP-Studio/Snap.Hutao.git
synced 2025-11-19 21:02:53 +08:00
Compare commits
36 Commits
feat/sopho
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f04f625482 | ||
|
|
d839a4c63c | ||
|
|
af7681c02d | ||
|
|
98278d6b4c | ||
|
|
22b33fcfb6 | ||
|
|
1bc7786dcb | ||
|
|
4ef65a2811 | ||
|
|
e7bcc6e3ae | ||
|
|
618f55acbc | ||
|
|
c761d8b7ad | ||
|
|
b6a0592102 | ||
|
|
57e042ec1c | ||
|
|
36e5885ed6 | ||
|
|
599ddd147c | ||
|
|
04cd4e7137 | ||
|
|
f3934ce2cd | ||
|
|
8dd74c6c89 | ||
|
|
ebbaf0e36a | ||
|
|
78726cd2ea | ||
|
|
707fc67e51 | ||
|
|
fbe6abc63a | ||
|
|
4e7f8e2a97 | ||
|
|
1ea92413f9 | ||
|
|
46c117edff | ||
|
|
9c4a9fc09a | ||
|
|
605fe5a3af | ||
|
|
7581cf8c8f | ||
|
|
6b67811bae | ||
|
|
6863cbb113 | ||
|
|
6b03ccdacc | ||
|
|
ad90c6b792 | ||
|
|
3ea7d59985 | ||
|
|
555043dfaa | ||
|
|
84e05017ba | ||
|
|
2c139a1ff6 | ||
|
|
04114fb170 |
@@ -89,6 +89,19 @@ public sealed class JsonSerializeTest
|
||||
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
|
||||
{
|
||||
public int A { get => B; set => B = value; }
|
||||
@@ -118,6 +131,18 @@ public sealed class JsonSerializeTest
|
||||
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))]
|
||||
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.Logging.Console" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.4.3" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.4.3" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="3.5.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="3.5.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -42,10 +42,6 @@ public sealed partial class App : Application
|
||||
private readonly IAppActivation activation;
|
||||
private readonly ILogger<App> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
public App(IServiceProvider serviceProvider)
|
||||
{
|
||||
// Load app resource
|
||||
@@ -63,7 +59,6 @@ public sealed partial class App : Application
|
||||
base.Exit();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
try
|
||||
|
||||
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)
|
||||
{
|
||||
return Enum.Parse<TEnum>(str);
|
||||
return Enum.Parse<TEnum>(str, ignoreCase: true);
|
||||
}
|
||||
|
||||
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> lazyInstalledLocation = new(() => Package.Current.InstalledLocation.Path);
|
||||
private readonly LazySlim<string> lazyFamilyName = new(() => Package.Current.Id.FamilyName);
|
||||
|
||||
private readonly LazySlim<bool> lazyToastAvailable = new(() =>
|
||||
@@ -100,8 +99,6 @@ internal sealed class RuntimeOptions
|
||||
|
||||
public string UserAgent { get => lazyVersionAndUserAgent.Value.UserAgent; }
|
||||
|
||||
public string InstalledLocation { get => lazyInstalledLocation.Value; }
|
||||
|
||||
public string DataFolder { get => lazyDataFolder.Value; }
|
||||
|
||||
public string LocalCache { get => lazyLocalCache.Value; }
|
||||
|
||||
@@ -65,6 +65,7 @@ internal static class SettingKeys
|
||||
public const string OverrideUpdateVersionComparison = "OverrideUpdateVersionComparison";
|
||||
public const string OverridePackageConvertDirectoryPermissionsRequirement = "OverridePackageConvertDirectoryPermissionsRequirement";
|
||||
public const string AlwaysIsFirstRunAfterUpdate = "AlwaysIsFirstRunAfterUpdate";
|
||||
public const string AlphaBuildUseCNPatchEndpoint = "AlphaBuildUseCNPatchEndpoint";
|
||||
#endregion
|
||||
|
||||
#region Obsolete
|
||||
|
||||
@@ -6,7 +6,6 @@ using Snap.Hutao.Win32.System.Com;
|
||||
using Snap.Hutao.Win32.UI.Shell;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using System.IO;
|
||||
using Windows.Storage;
|
||||
using static Snap.Hutao.Win32.Macros;
|
||||
using static Snap.Hutao.Win32.Ole32;
|
||||
|
||||
@@ -18,27 +17,23 @@ internal sealed partial class ShellLinkInterop : IShellLinkInterop
|
||||
{
|
||||
private readonly RuntimeOptions runtimeOptions;
|
||||
|
||||
public async ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
|
||||
public ValueTask<bool> TryCreateDesktopShoutcutForElevatedLaunchAsync()
|
||||
{
|
||||
string targetLogoPath = Path.Combine(runtimeOptions.DataFolder, "ShellLinkLogo.ico");
|
||||
string elevatedLauncherPath = Path.Combine(runtimeOptions.DataFolder, "Snap.Hutao.Elevated.Launcher.exe");
|
||||
|
||||
try
|
||||
{
|
||||
Uri sourceLogoUri = "ms-appx:///Assets/Logo.ico".ToUri();
|
||||
StorageFile iconFile = await StorageFile.GetFileFromApplicationUriAsync(sourceLogoUri);
|
||||
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);
|
||||
File.Copy(InstalledLocation.GetAbsolutePath("Assets/Logo.ico"), targetLogoPath, true);
|
||||
File.Copy(InstalledLocation.GetAbsolutePath("Snap.Hutao.Elevated.Launcher.exe"), elevatedLauncherPath, true);
|
||||
}
|
||||
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)
|
||||
|
||||
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;
|
||||
@@ -3,26 +3,12 @@
|
||||
|
||||
namespace Snap.Hutao.Model.Calculable;
|
||||
|
||||
/// <summary>
|
||||
/// 可计算物品选项
|
||||
/// </summary>
|
||||
internal readonly struct CalculableOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色
|
||||
/// </summary>
|
||||
public readonly ICalculableAvatar? Avatar;
|
||||
|
||||
/// <summary>
|
||||
/// 武器
|
||||
/// </summary>
|
||||
public readonly ICalculableWeapon? Weapon;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的可计算物品选项
|
||||
/// </summary>
|
||||
/// <param name="avatar">角色</param>
|
||||
/// <param name="weapon">武器</param>
|
||||
public CalculableOptions(ICalculableAvatar? avatar, ICalculableWeapon? weapon)
|
||||
{
|
||||
Avatar = avatar;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -12,4 +12,10 @@ internal static class LevelFormat
|
||||
{
|
||||
return $"Lv.{value}";
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static string Format(uint value, uint extra)
|
||||
{
|
||||
return extra > 0 ? $"Lv.{value + extra} ({value} +{extra})" : $"Lv.{value}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutao"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.10.6.0" />
|
||||
Version="1.10.7.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao</DisplayName>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<Identity
|
||||
Name="60568DGPStudio.SnapHutaoDev"
|
||||
Publisher="CN=35C8E923-85DF-49A7-9172-B39DC6312C52"
|
||||
Version="1.10.6.0" />
|
||||
Version="1.10.7.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Snap Hutao Dev</DisplayName>
|
||||
|
||||
@@ -12,9 +12,6 @@ using WinRT;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
/// <summary>
|
||||
/// Program class
|
||||
/// </summary>
|
||||
[SuppressMessage("", "SH001")]
|
||||
public static partial class Program
|
||||
{
|
||||
|
||||
@@ -1247,6 +1247,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>Add or update to current Enhancement Progression Plan</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>Always create new adopted target items</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>Save Method</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>Overwrite existing adopted items</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>Keep existing target items</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>Daily Commission Availability Notification</value>
|
||||
</data>
|
||||
@@ -1598,6 +1610,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>Operation not fully completed, added/updated {0}, skipped {1}</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>The selected level does not require ingredients</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>No plan has been created and selected</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>There is already a foster project for this item</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>Successfully added to current plan</value>
|
||||
</data>
|
||||
@@ -1818,10 +1839,10 @@
|
||||
<value>Export failed</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFExportSuccess" xml:space="preserve">
|
||||
<value>Export successfully</value>
|
||||
<value>Exported successfully</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportDuplicatedHk4eEntry" xml:space="preserve">
|
||||
<value>Imported UIGF files contain UID duplicated prayers</value>
|
||||
<value>Imported UIGF files contain UID duplicated wish items</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportError" xml:space="preserve">
|
||||
<value>Import failed</value>
|
||||
@@ -2847,7 +2868,7 @@
|
||||
<value>Import</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
|
||||
<value>在「祈愿记录-角色」与「祈愿记录-武器」中显示未抽取到的祈愿物品</value>
|
||||
<value>Display Undrawn Items in "Wish Export - Characters" and "Wish Export - Weapons"</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" xml:space="preserve">
|
||||
<value>Undrawn Wish Items</value>
|
||||
|
||||
@@ -1247,6 +1247,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委托上线提醒</value>
|
||||
</data>
|
||||
@@ -1598,6 +1610,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功添加至当前养成计划</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,6 +1247,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>Tambahkan atau perbarui ke Rencana Dev saat ini</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>Pemberitahuan Ketersediaan Komisi Harian</value>
|
||||
</data>
|
||||
@@ -1598,6 +1610,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>Operasi tidak sepenuhnya selesai, ditambahkan/diperbarui {0}, dilewati {1}</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>Berhasil ditambahkan ke rencana saat ini.</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,6 +1247,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>現在の育成計画に追加または更新する</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>アイテムは常に新しいドリルを作成</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>次で保存</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>既存の育成目的アイテム</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>対象のアイテムを予約する</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>まだデイリー依頼を行っていません</value>
|
||||
</data>
|
||||
@@ -1598,6 +1610,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作の一部に失敗しました:追加/更新:{0}、スキップ{1}</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>選択したLvでは育成材料は必要ありません</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>まだ作成されておらず、養成計画を選択してください</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>そのアイテムは既に適用されているアイテムがあります</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>選択中の育成計画に正常に追加されました</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,6 +1247,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>일일 의뢰 가능 알림</value>
|
||||
</data>
|
||||
@@ -1598,6 +1610,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>현재 육성 계획 추가에 성공했습니다</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,6 +1247,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>Adicionar ou atualizar ao planejamento atual</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>Notificação diária de disponibilidade de missão</value>
|
||||
</data>
|
||||
@@ -1598,6 +1610,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>Operação não totalmente concluída, adicionado/atualizado {0}, pulado {1}</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>Adicionado com sucesso ao planejamento</value>
|
||||
</data>
|
||||
|
||||
@@ -1010,6 +1010,9 @@
|
||||
<data name="ServiceGamePackageConvertMoveFileRestoreFormat" xml:space="preserve">
|
||||
<value>替换:{0}</value>
|
||||
</data>
|
||||
<data name="ServiceGamePackageReadLocalPackageVerionFailed" xml:space="preserve">
|
||||
<value>在读取本地 Package Version 时遇到问题,修复游戏可能会有所帮助</value>
|
||||
</data>
|
||||
<data name="ServiceGamePackageRenameDataFolderFailed" xml:space="preserve">
|
||||
<value>重命名数据文件夹名称失败</value>
|
||||
</data>
|
||||
@@ -1247,6 +1250,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委托上线提醒</value>
|
||||
</data>
|
||||
@@ -1424,6 +1439,15 @@
|
||||
<data name="ViewDialogUpdatePackageDownloadUpdatelogLinkContent" xml:space="preserve">
|
||||
<value>查看更新日志</value>
|
||||
</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">
|
||||
<value>立即前往</value>
|
||||
</data>
|
||||
@@ -1523,6 +1547,9 @@
|
||||
<data name="ViewInfoBarPanelContractContent" xml:space="preserve">
|
||||
<value>收起</value>
|
||||
</data>
|
||||
<data name="ViewInfoBarPanelTitle" xml:space="preserve">
|
||||
<value>所有通知</value>
|
||||
</data>
|
||||
<data name="ViewInfoBarToggleTitle" xml:space="preserve">
|
||||
<value>有新的通知</value>
|
||||
</data>
|
||||
@@ -1598,6 +1625,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加 / 更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功添加至当前养成计划</value>
|
||||
</data>
|
||||
@@ -2408,6 +2444,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>
|
||||
@@ -2492,8 +2540,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>
|
||||
@@ -3224,6 +3278,9 @@
|
||||
<data name="WebDailyNoteResinRecoveryFormat" xml:space="preserve">
|
||||
<value>将于 {0} {1:HH:mm} 后全部恢复</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteStoredAttendanceRefreshCountdown" xml:space="preserve">
|
||||
<value>{0:c} 后重置</value>
|
||||
</data>
|
||||
<data name="WebDailyNoteTransformerAppend" xml:space="preserve">
|
||||
<value>后可再次使用</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,6 +1247,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委托上线提醒</value>
|
||||
</data>
|
||||
@@ -1598,6 +1610,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功添加至当前养成计划</value>
|
||||
</data>
|
||||
|
||||
@@ -1247,6 +1247,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到当前养成计划</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委托上线提醒</value>
|
||||
</data>
|
||||
@@ -1598,6 +1610,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 个,跳过 {1} 个</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功添加至当前养成计划</value>
|
||||
</data>
|
||||
|
||||
@@ -557,10 +557,10 @@
|
||||
<value>必須登入 米游社/ HoYoLAB 並選擇一個用戶與角色</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceDeleteEntrySucceed" xml:space="preserve">
|
||||
<value>刪除了 UID:{0} 的 {1} 筆祈願記錄</value>
|
||||
<value>刪除了 UID:{0} 的 {1} 筆祈願紀錄</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientRecordSlot" xml:space="preserve">
|
||||
<value>胡桃雲儲存的祈願記錄存檔數已達當前帳號上限</value>
|
||||
<value>胡桃雲儲存的祈願紀錄存檔數已達當前帳號上限</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceInsufficientTime" xml:space="preserve">
|
||||
<value>未開通祈願紀錄上傳服務或已到期</value>
|
||||
@@ -572,7 +572,7 @@
|
||||
<value>數據異常,無法儲存至雲端,請勿跨帳號上傳或嘗試删除雲端數據後重試</value>
|
||||
</data>
|
||||
<data name="ServerGachaLogServiceUploadEntrySucceed" xml:space="preserve">
|
||||
<value>上傳了 UID:{0} 的 {1} 筆祈願記錄,儲存了 {2} 筆</value>
|
||||
<value>上傳了 UID:{0} 的 {1} 筆祈願紀錄,儲存了 {2} 筆</value>
|
||||
</data>
|
||||
<data name="ServerPassportLoginRequired" xml:space="preserve">
|
||||
<value>請先登入或註冊胡桃帳號</value>
|
||||
@@ -623,40 +623,40 @@
|
||||
<value>驗證請求過快,請 1 分鐘後再試</value>
|
||||
</data>
|
||||
<data name="ServerRecordBannedUid" xml:space="preserve">
|
||||
<value>上傳深淵記錄失敗,當前 UID 已被胡桃數據庫封禁</value>
|
||||
<value>上傳深淵紀錄失敗,當前 UID 已被胡桃數據庫封禁</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics" xml:space="preserve">
|
||||
<value>上傳深淵記錄失敗,正在計算統計數據</value>
|
||||
<value>上傳深淵紀錄失敗,正在計算統計數據</value>
|
||||
</data>
|
||||
<data name="ServerRecordComputingStatistics2" xml:space="preserve">
|
||||
<value>獲取數據失敗,正在計算統計數據</value>
|
||||
</data>
|
||||
<data name="ServerRecordInternalException" xml:space="preserve">
|
||||
<value>上傳深淵記錄失敗,伺服器異常,請盡快聯繫開發者解決</value>
|
||||
<value>上傳深淵紀錄失敗,伺服器異常,請盡快聯繫開發者解決</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidData" xml:space="preserve">
|
||||
<value>上傳深淵記錄失敗,存在無效的數據</value>
|
||||
<value>上傳深淵紀錄失敗,存在無效的數據</value>
|
||||
</data>
|
||||
<data name="ServerRecordInvalidUid" xml:space="preserve">
|
||||
<value>無效的 UID</value>
|
||||
</data>
|
||||
<data name="ServerRecordNotCurrentSchedule" xml:space="preserve">
|
||||
<value>上傳深淵記錄失敗,不是本期數據</value>
|
||||
<value>上傳深淵紀錄失敗,不是本期數據</value>
|
||||
</data>
|
||||
<data name="ServerRecordPreviousRequestNotCompleted" xml:space="preserve">
|
||||
<value>上傳深淵記錄失敗,當前 UID 的紀錄仍在處理中,請勿重複操作</value>
|
||||
<value>上傳深淵紀錄失敗,當前 UID 的紀錄仍在處理中,請勿重複操作</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessAndGachaLogServiceTimeExtended" xml:space="preserve">
|
||||
<value>上傳深淵記錄成功,獲贈祈願記錄上傳服務時長</value>
|
||||
<value>上傳深淵紀錄成功,獲贈祈願紀錄上傳服務時長</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNoPassport" xml:space="preserve">
|
||||
<value>上傳深淵記錄成功,但未登入胡桃通行證,無法獲贈祈願記錄上傳服務時長</value>
|
||||
<value>上傳深淵紀錄成功,但未登入胡桃通行證,無法獲贈祈願紀錄上傳服務時長</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNoSuchUser" xml:space="preserve">
|
||||
<value>上傳深淵記錄成功,但無法找到用戶,無法獲贈祈願記錄上傳服務時長</value>
|
||||
<value>上傳深淵紀錄成功,但無法找到用戶,無法獲贈祈願紀錄上傳服務時長</value>
|
||||
</data>
|
||||
<data name="ServerRecordUploadSuccessButNotFirstTimeAtCurrentSchedule" xml:space="preserve">
|
||||
<value>上傳深淵記錄成功,但不是本期首次提交,無法獲贈祈願記錄上傳服務時長</value>
|
||||
<value>上傳深淵紀錄成功,但不是本期首次提交,無法獲贈祈願紀錄上傳服務時長</value>
|
||||
</data>
|
||||
<data name="ServiceAchievementImportResultFormat" xml:space="preserve">
|
||||
<value>新增:{0} 個成就 | 更新:{1} 個成就 | 删除:{2} 個成就</value>
|
||||
@@ -878,10 +878,10 @@
|
||||
<value>由 {0} 啟動</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogArchiveCollectionUserdataCorruptedMessage" xml:space="preserve">
|
||||
<value>無法獲取祈願記錄:{0}</value>
|
||||
<value>無法獲取祈願紀錄:{0}</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogEndIdUserdataCorruptedMessage" xml:space="preserve">
|
||||
<value>無法獲取祈願記錄 End Id</value>
|
||||
<value>無法獲取祈願紀錄 End Id</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogFactoryAvatarWishName" xml:space="preserve">
|
||||
<value>角色活動</value>
|
||||
@@ -899,7 +899,7 @@
|
||||
<value>獲取雲端祈願紀錄失敗</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogHutaoCloudServiceNotAllowed" xml:space="preserve">
|
||||
<value>祈願記錄上傳服務不可用</value>
|
||||
<value>祈願紀錄上傳服務不可用</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogUIGFImportItemInvalidFormat" xml:space="preserve">
|
||||
<value>數據包含異常物品, Id:{0}</value>
|
||||
@@ -921,7 +921,7 @@
|
||||
<value>提供的 URL 無效</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogUrlProviderStokenUnsupported" xml:space="preserve">
|
||||
<value>HoYoLAB 帳號不支持使用 SToken 重新整理祈願記錄</value>
|
||||
<value>HoYoLAB 帳號不支持使用 SToken 重新整理祈願紀錄</value>
|
||||
</data>
|
||||
<data name="ServiceGachaLogUrlProviderUrlLanguageNotMatchCurrentLocale" xml:space="preserve">
|
||||
<value>URL 中的語言:{0} 與胡桃的語言:{1} 不對應,請切換到對應語言重試</value>
|
||||
@@ -1247,6 +1247,18 @@
|
||||
<data name="ViewDialogCultivatePromotionDeltaTitle" xml:space="preserve">
|
||||
<value>添加或更新到當前養成計劃</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry" xml:space="preserve">
|
||||
<value>总是创建新的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyHeader" xml:space="preserve">
|
||||
<value>保存方式</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting" xml:space="preserve">
|
||||
<value>覆盖存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogCultivationConsumptionSaveStrategyPreserveExisting" xml:space="preserve">
|
||||
<value>保留存在的养成目标物品</value>
|
||||
</data>
|
||||
<data name="ViewDialogDailyNoteNotificationDailyTaskNotify" xml:space="preserve">
|
||||
<value>每日委託上線提醒</value>
|
||||
</data>
|
||||
@@ -1275,10 +1287,10 @@
|
||||
<value>即時便箋 Webhook URL</value>
|
||||
</data>
|
||||
<data name="ViewDialogExportUIGFSubtitle" xml:space="preserve">
|
||||
<value>选择要导出记录的 UID</value>
|
||||
<value>選擇要匯出紀錄的 UID</value>
|
||||
</data>
|
||||
<data name="ViewDialogExportUIGFTitle" xml:space="preserve">
|
||||
<value>导出 UIGF 文件</value>
|
||||
<value>匯出 UIGF 文件</value>
|
||||
</data>
|
||||
<data name="ViewDialogFeedbackEnableLoopbackContent" xml:space="preserve">
|
||||
<value>解除限制後需使用其他工具恢復限制</value>
|
||||
@@ -1287,10 +1299,10 @@
|
||||
<value>是否解除 Loopback 限制</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogImportTitle" xml:space="preserve">
|
||||
<value>匯入祈願記錄</value>
|
||||
<value>匯入祈願紀錄</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogRefreshProgressAuthkeyTimeout" xml:space="preserve">
|
||||
<value>祈願記錄 URL 已失效,請重新獲取</value>
|
||||
<value>祈願紀錄 URL 已失效,請重新獲取</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogRefreshProgressDescription" xml:space="preserve">
|
||||
<value>正在獲取 {0}</value>
|
||||
@@ -1302,7 +1314,7 @@
|
||||
<value>請輸入 URL</value>
|
||||
</data>
|
||||
<data name="ViewDialogGachaLogUrlTitle" xml:space="preserve">
|
||||
<value>手動輸入祈願記錄 URL</value>
|
||||
<value>手動輸入祈願紀錄 URL</value>
|
||||
</data>
|
||||
<data name="ViewDialogGeetestCustomUrlCompositInputHint" xml:space="preserve">
|
||||
<value>請輸入請求接口的 URL 複合模板</value>
|
||||
@@ -1368,10 +1380,10 @@
|
||||
<value>UIGF 版本</value>
|
||||
</data>
|
||||
<data name="ViewDialogImportUIGFSubtitle" xml:space="preserve">
|
||||
<value>选择要导入记录的 UID</value>
|
||||
<value>選擇要匯入紀錄的 UID</value>
|
||||
</data>
|
||||
<data name="ViewDialogImportUIGFTitle" xml:space="preserve">
|
||||
<value>导入 UIGF 文件</value>
|
||||
<value>匯入 UIGF 檔案</value>
|
||||
</data>
|
||||
<data name="ViewDialogLaunchGameAccountInputPlaceholder" xml:space="preserve">
|
||||
<value>在此處輸入名稱</value>
|
||||
@@ -1443,7 +1455,7 @@
|
||||
<value>回饋中心</value>
|
||||
</data>
|
||||
<data name="ViewGachaLogHeader" xml:space="preserve">
|
||||
<value>祈願記錄</value>
|
||||
<value>祈願紀錄</value>
|
||||
</data>
|
||||
<data name="ViewGuideStaticResourceDownloadSize" xml:space="preserve">
|
||||
<value>預計下載大小:{0}</value>
|
||||
@@ -1598,6 +1610,15 @@
|
||||
<data name="ViewModelCultivationBatchAddIncompletedFormat" xml:space="preserve">
|
||||
<value>操作未全部完成:添加/更新:{0} 個,跳過 {1} 個</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoItemHint" xml:space="preserve">
|
||||
<value>选定的等级不需要养成材料</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveNoProjectHint" xml:space="preserve">
|
||||
<value>尚未创建并选择养成计划</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationConsumptionSaveSkippedHint" xml:space="preserve">
|
||||
<value>已存在该物品的养成项目</value>
|
||||
</data>
|
||||
<data name="ViewModelCultivationEntryAddSuccess" xml:space="preserve">
|
||||
<value>已成功新增至目前養成計劃</value>
|
||||
</data>
|
||||
@@ -1821,19 +1842,19 @@
|
||||
<value>匯出成功</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportDuplicatedHk4eEntry" xml:space="preserve">
|
||||
<value>导入的 UIGF 文件中包含 UID 重复的祈愿记录项</value>
|
||||
<value>匯入的 UIGF 檔案中包含 UID 重複的祈願紀錄項</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportError" xml:space="preserve">
|
||||
<value>匯入失敗</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportNoHk4eEntry" xml:space="preserve">
|
||||
<value>导入的 UIGF 文件中不包含祈愿数据</value>
|
||||
<value>匯入的 UIGF 檔案中不包含祈願數據</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportNoSelectedEntry" xml:space="preserve">
|
||||
<value>请选择至少一个 UID 以导入数据</value>
|
||||
<value>請選擇至少一個 UID 以匯入數據</value>
|
||||
</data>
|
||||
<data name="ViewModelUIGFImportSuccess" xml:space="preserve">
|
||||
<value>导入成功</value>
|
||||
<value>匯入成功</value>
|
||||
</data>
|
||||
<data name="ViewModelUserAdded" xml:space="preserve">
|
||||
<value>用戶 [{0}] 新增成功</value>
|
||||
@@ -2193,7 +2214,7 @@
|
||||
<value>手動輸入 URL</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRefreshByManualInputDescription" xml:space="preserve">
|
||||
<value>使用由你提供的 URL 重新整理祈願記錄</value>
|
||||
<value>使用由你提供的 URL 重新整理祈願紀錄</value>
|
||||
</data>
|
||||
<data name="ViewPageGachaLogRefreshBySToken" xml:space="preserve">
|
||||
<value>SToken 重新整理</value>
|
||||
@@ -2286,7 +2307,7 @@
|
||||
<value>數據收集統計</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewRecordTotal" xml:space="preserve">
|
||||
<value>上傳記錄總數</value>
|
||||
<value>上傳紀錄總數</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewRefreshTime" xml:space="preserve">
|
||||
<value>數據重新整理時間</value>
|
||||
@@ -2298,16 +2319,16 @@
|
||||
<value>平均戰鬥次數</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssFullStar" xml:space="preserve">
|
||||
<value>滿星深淵記錄</value>
|
||||
<value>滿星深淵紀錄</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssPassed" xml:space="preserve">
|
||||
<value>通關深淵記錄</value>
|
||||
<value>通關深淵紀錄</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssStarAverage" xml:space="preserve">
|
||||
<value>平均獲取淵星</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewSpiralAbyssTotal" xml:space="preserve">
|
||||
<value>總計深淵記錄</value>
|
||||
<value>總計深淵紀錄</value>
|
||||
</data>
|
||||
<data name="ViewPageHutaoDatabaseOverviewTeamAppearance" xml:space="preserve">
|
||||
<value>隊伍出場</value>
|
||||
@@ -2610,7 +2631,7 @@
|
||||
<value>刪除遊戲内網頁快取</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingDeleteUserDescription" xml:space="preserve">
|
||||
<value>直接刪除用戶表的所有記錄,用於修復特定的賬號衝突問題</value>
|
||||
<value>直接刪除用戶表的所有紀錄,用於修復特定的賬號衝突問題</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingDeleteUserHeader" xml:space="preserve">
|
||||
<value>刪除所有用戶</value>
|
||||
@@ -2652,7 +2673,7 @@
|
||||
<value>前往反饋</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingGachaLogHeader" xml:space="preserve">
|
||||
<value>祈願記錄</value>
|
||||
<value>祈願紀錄</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingGameHeader" xml:space="preserve">
|
||||
<value>遊戲</value>
|
||||
@@ -2838,16 +2859,16 @@
|
||||
<value>匯出</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUIGFExportImportDescription" xml:space="preserve">
|
||||
<value>导出/导入 UIGF 4 文件</value>
|
||||
<value>匯出/匯入 UIGF 4 檔案</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUIGFExportImportHeader" xml:space="preserve">
|
||||
<value>数据迁移</value>
|
||||
<value>數據遷移</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUIGFImportContent" xml:space="preserve">
|
||||
<value>匯入</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleDescription" xml:space="preserve">
|
||||
<value>在祈願記錄頁面角色與武器頁籤顯示未抽取到的祈願物品</value>
|
||||
<value>在「祈願紀錄 - 角色」與「祈願紀錄 - 武器」中顯示未抽到的祈願物品</value>
|
||||
</data>
|
||||
<data name="ViewPageSettingUnobtainedWishItemVisibleHeader" 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;
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,10 @@ using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.InterChange.Achievement;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.ViewModel.Achievement;
|
||||
using System.Collections.ObjectModel;
|
||||
using EntityAchievement = Snap.Hutao.Model.Entity.Achievement;
|
||||
|
||||
namespace Snap.Hutao.Service.Achievement;
|
||||
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Scoped, typeof(IAchievementService))]
|
||||
internal sealed partial class AchievementService : IAchievementService
|
||||
@@ -31,9 +29,7 @@ internal sealed partial class AchievementService : IAchievementService
|
||||
if (archives is null)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
ObservableCollection<AchievementArchive> source = achievementDbService.GetAchievementArchiveCollection();
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
archives = new(source, serviceProvider);
|
||||
archives = new(achievementDbService.GetAchievementArchiveCollection(), serviceProvider);
|
||||
}
|
||||
|
||||
return archives;
|
||||
|
||||
@@ -7,6 +7,7 @@ using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Converter;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||
using System.Collections.Immutable;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory.Builder;
|
||||
|
||||
@@ -199,30 +200,29 @@ internal static class AvatarViewBuilderExtension
|
||||
return [];
|
||||
}
|
||||
|
||||
IReadOnlyDictionary<SkillGroupId, SkillLevel> extraLevelMap = proudSkillExtraLevelMap as IReadOnlyDictionary<SkillGroupId, SkillLevel> ?? ImmutableDictionary<SkillGroupId, SkillLevel>.Empty;
|
||||
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
|
||||
return new SkillView()
|
||||
{
|
||||
Name = proudableSkill.Name,
|
||||
Icon = SkillIconConverter.IconNameToUri(proudableSkill.Icon),
|
||||
Description = proudableSkill.Description,
|
||||
Name = proudSkill.Name,
|
||||
Icon = SkillIconConverter.IconNameToUri(proudSkill.Icon),
|
||||
Description = proudSkill.Description,
|
||||
|
||||
GroupId = proudableSkill.GroupId,
|
||||
GroupId = proudSkill.GroupId,
|
||||
Level = LevelFormat.Format(skillLevelMap[skillId], extraLevelMap.GetValueOrDefault(proudSkill.GroupId)),
|
||||
LevelNumber = skillLevelMap[skillId],
|
||||
Info = DescriptionsParametersDescriptor.Convert(proudableSkill.Proud, skillExtraLeveledMap[skillId]),
|
||||
Info = DescriptionsParametersDescriptor.Convert(proudSkill.Proud, skillExtraLeveledMap[skillId]),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
using Snap.Hutao.UI.Xaml.Data;
|
||||
using Snap.Hutao.ViewModel.AvatarProperty;
|
||||
|
||||
namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
@@ -17,7 +18,6 @@ namespace Snap.Hutao.Service.AvatarInfo.Factory;
|
||||
internal sealed partial class SummaryFactory : ISummaryFactory
|
||||
{
|
||||
private readonly IMetadataService metadataService;
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<Summary> CreateAsync(IEnumerable<Model.Entity.AvatarInfo> avatarInfos, CancellationToken token)
|
||||
@@ -37,11 +37,9 @@ internal sealed partial class SummaryFactory : ISummaryFactory
|
||||
|
||||
IList<AvatarView> views = [.. avatars];
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
return new()
|
||||
{
|
||||
Avatars = new(views),
|
||||
Avatars = views.ToAdvancedCollectionView(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
internal enum ConsumptionSaveResultKind
|
||||
{
|
||||
NoItem,
|
||||
NoProject,
|
||||
Skipped,
|
||||
Added,
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
internal enum ConsumptionSaveStrategyKind
|
||||
{
|
||||
PreserveExisting,
|
||||
OverwriteExisting,
|
||||
CreateNewEntry,
|
||||
}
|
||||
@@ -8,7 +8,6 @@ using Snap.Hutao.Service.Inventory;
|
||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
using Snap.Hutao.ViewModel.Cultivation;
|
||||
using System.Collections.ObjectModel;
|
||||
using CalculateItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
|
||||
using ModelItem = Snap.Hutao.Model.Item;
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
@@ -115,45 +114,54 @@ internal sealed partial class CultivationService : ICultivationService
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<CalculateItem> items, LevelInformation levelInformation)
|
||||
public async ValueTask<ConsumptionSaveResultKind> SaveConsumptionAsync(InputConsumption inputConsumption)
|
||||
{
|
||||
if (items.Count == 0)
|
||||
if (inputConsumption.Items.Count == 0)
|
||||
{
|
||||
return true;
|
||||
return ConsumptionSaveResultKind.NoItem;
|
||||
}
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
if (Projects.CurrentItem is null)
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Projects.MoveCurrentTo(Projects.SourceCollection.SelectedOrDefault());
|
||||
if (Projects.CurrentItem is null)
|
||||
{
|
||||
return false;
|
||||
return ConsumptionSaveResultKind.NoProject;
|
||||
}
|
||||
}
|
||||
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
CultivateEntry? entry = type is CultivateType.AvatarAndSkill
|
||||
? cultivationDbService.GetCultivateEntryByProjectIdAndItemId(Projects.CurrentItem.InnerId, itemId)
|
||||
: default;
|
||||
|
||||
CultivateEntry? entry = default;
|
||||
|
||||
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting or ConsumptionSaveStrategyKind.OverwriteExisting)
|
||||
{
|
||||
entry = cultivationDbService.GetCultivateEntryByProjectIdAndItemId(Projects.CurrentItem.InnerId, inputConsumption.ItemId);
|
||||
|
||||
if (inputConsumption.Strategy is ConsumptionSaveStrategyKind.PreserveExisting && entry is not null)
|
||||
{
|
||||
return ConsumptionSaveResultKind.Skipped;
|
||||
}
|
||||
}
|
||||
|
||||
if (entry is null)
|
||||
{
|
||||
entry = CultivateEntry.From(Projects.CurrentItem.InnerId, type, itemId);
|
||||
entry = CultivateEntry.From(Projects.CurrentItem.InnerId, inputConsumption.Type, inputConsumption.ItemId);
|
||||
cultivationDbService.AddCultivateEntry(entry);
|
||||
}
|
||||
|
||||
Guid entryId = entry.InnerId;
|
||||
|
||||
cultivationDbService.RemoveLevelInformationByEntryId(entryId);
|
||||
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, type, levelInformation);
|
||||
CultivateEntryLevelInformation entryLevelInformation = CultivateEntryLevelInformation.From(entryId, inputConsumption.Type, inputConsumption.LevelInformation);
|
||||
cultivationDbService.AddLevelInformation(entryLevelInformation);
|
||||
|
||||
cultivationDbService.RemoveCultivateItemRangeByEntryId(entryId);
|
||||
IEnumerable<CultivateItem> toAdd = items.Select(item => CultivateItem.From(entryId, item));
|
||||
IEnumerable<CultivateItem> toAdd = inputConsumption.Items.Select(item => CultivateItem.From(entryId, item));
|
||||
cultivationDbService.AddCultivateItemRange(toAdd);
|
||||
|
||||
return true;
|
||||
return ConsumptionSaveResultKind.Added;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -3,22 +3,13 @@
|
||||
|
||||
using Snap.Hutao.Core.Database;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using Snap.Hutao.ViewModel.Cultivation;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
/// <summary>
|
||||
/// 养成计算服务
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
internal interface ICultivationService
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取用于绑定的项目集合
|
||||
/// </summary>
|
||||
AdvancedDbCollectionView<CultivateProject> Projects { get; }
|
||||
|
||||
ValueTask<ObservableCollection<CultivateEntryView>> GetCultivateEntriesAsync(CultivateProject cultivateProject, ICultivationMetadataContext context);
|
||||
@@ -26,32 +17,13 @@ internal interface ICultivationService
|
||||
ValueTask<ObservableCollection<StatisticsCultivateItem>> GetStatisticsCultivateItemCollectionAsync(
|
||||
CultivateProject cultivateProject, ICultivationMetadataContext context, CancellationToken token);
|
||||
|
||||
/// <summary>
|
||||
/// 删除养成清单
|
||||
/// </summary>
|
||||
/// <param name="entryId">入口Id</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask RemoveCultivateEntryAsync(Guid entryId);
|
||||
|
||||
/// <summary>
|
||||
/// 异步移除项目
|
||||
/// </summary>
|
||||
/// <param name="project">项目</param>
|
||||
/// <returns>任务</returns>
|
||||
ValueTask RemoveProjectAsync(CultivateProject project);
|
||||
|
||||
ValueTask<bool> SaveConsumptionAsync(CultivateType type, uint itemId, List<Item> items, LevelInformation levelInformation);
|
||||
ValueTask<ConsumptionSaveResultKind> SaveConsumptionAsync(InputConsumption inputConsumption);
|
||||
|
||||
/// <summary>
|
||||
/// 保存养成物品状态
|
||||
/// </summary>
|
||||
/// <param name="item">养成物品</param>
|
||||
void SaveCultivateItem(CultivateItemView item);
|
||||
|
||||
/// <summary>
|
||||
/// 异步尝试添加新的项目
|
||||
/// </summary>
|
||||
/// <param name="project">项目</param>
|
||||
/// <returns>添加操作的结果</returns>
|
||||
ValueTask<ProjectAddResultKind> TryAddProjectAsync(CultivateProject project);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Entity.Primitive;
|
||||
using CalculateItem = Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate.Item;
|
||||
|
||||
namespace Snap.Hutao.Service.Cultivation;
|
||||
|
||||
internal sealed class InputConsumption
|
||||
{
|
||||
public required CultivateType Type { get; init; }
|
||||
|
||||
public required uint ItemId { get; init; }
|
||||
|
||||
public required List<CalculateItem> Items { get; init; }
|
||||
|
||||
public required LevelInformation LevelInformation { get; init; }
|
||||
|
||||
public required ConsumptionSaveStrategyKind Strategy { get; init; }
|
||||
}
|
||||
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();
|
||||
}
|
||||
@@ -203,7 +203,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
|
||||
AsyncBarrier barrier = new(4);
|
||||
|
||||
List<HistoryWish> historyWishes = historyWishBuilders
|
||||
.Where(b => appOptions.IsEmptyHistoryWishVisible || (!b.IsEmpty))
|
||||
.Where(b => appOptions.IsEmptyHistoryWishVisible || !b.IsEmpty)
|
||||
.OrderByDescending(builder => builder.From)
|
||||
.ThenBy(builder => builder.ConfigType, GachaTypeComparer.Shared)
|
||||
.Select(builder => builder.ToHistoryWish())
|
||||
@@ -212,7 +212,7 @@ internal sealed partial class GachaStatisticsFactory : IGachaStatisticsFactory
|
||||
return new()
|
||||
{
|
||||
// history
|
||||
HistoryWishes = taskContext.InvokeOnMainThread(() => new AdvancedCollectionView<HistoryWish>(historyWishes)),
|
||||
HistoryWishes = historyWishes.ToAdvancedCollectionView(),
|
||||
|
||||
// avatars
|
||||
OrangeAvatars = orangeAvatarCounter.ToStatisticsList(),
|
||||
|
||||
@@ -47,7 +47,6 @@ internal sealed partial class GachaLogService : IGachaLogService
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
context = await metadataService.GetContextAsync<GachaLogServiceMetadataContext>(token).ConfigureAwait(false);
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
Archives = new(gachaLogDbService.GetGachaArchiveCollection(), serviceProvider);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -21,9 +21,15 @@ internal sealed partial class GameAccountService : IGameAccountService
|
||||
|
||||
private ObservableReorderableDbCollection<GameAccount>? gameAccounts;
|
||||
|
||||
public ObservableReorderableDbCollection<GameAccount> GameAccountCollection
|
||||
public async ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync()
|
||||
{
|
||||
get => gameAccounts ??= gameDbService.GetGameAccountCollection();
|
||||
if (gameAccounts is null)
|
||||
{
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
gameAccounts = gameDbService.GetGameAccountCollection();
|
||||
}
|
||||
|
||||
return gameAccounts;
|
||||
}
|
||||
|
||||
public async ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType)
|
||||
|
||||
@@ -9,14 +9,14 @@ namespace Snap.Hutao.Service.Game.Account;
|
||||
|
||||
internal interface IGameAccountService
|
||||
{
|
||||
ObservableReorderableDbCollection<GameAccount> GameAccountCollection { get; }
|
||||
|
||||
ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid);
|
||||
|
||||
GameAccount? DetectCurrentGameAccount(SchemeType schemeType);
|
||||
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType schemeType);
|
||||
|
||||
ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync();
|
||||
|
||||
ValueTask ModifyGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
@@ -11,10 +11,6 @@ using Snap.Hutao.Service.Game.PathAbstraction;
|
||||
|
||||
namespace Snap.Hutao.Service.Game;
|
||||
|
||||
/// <summary>
|
||||
/// 游戏服务
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[ConstructorGenerated]
|
||||
[Injection(InjectAs.Singleton, typeof(IGameServiceFacade))]
|
||||
internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
@@ -23,55 +19,46 @@ internal sealed partial class GameServiceFacade : IGameServiceFacade
|
||||
private readonly IGameAccountService gameAccountService;
|
||||
private readonly IGamePathService gamePathService;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ObservableReorderableDbCollection<GameAccount> GameAccountCollection
|
||||
public ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync()
|
||||
{
|
||||
get => gameAccountService.GameAccountCollection;
|
||||
return gameAccountService.GetGameAccountCollectionAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<ValueResult<bool, string>> GetGamePathAsync()
|
||||
{
|
||||
return gamePathService.SilentGetGamePathAsync();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ChannelOptions GetChannelOptions()
|
||||
{
|
||||
return gameChannelOptionsService.GetChannelOptions();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme)
|
||||
{
|
||||
return gameAccountService.DetectGameAccountAsync(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public GameAccount? DetectCurrentGameAccount(SchemeType scheme)
|
||||
{
|
||||
return gameAccountService.DetectCurrentGameAccount(scheme);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid)
|
||||
{
|
||||
return gameAccountService.AttachGameAccountToUidAsync(gameAccount, uid);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask ModifyGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
return gameAccountService.ModifyGameAccountAsync(gameAccount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public ValueTask RemoveGameAccountAsync(GameAccount gameAccount)
|
||||
{
|
||||
return gameAccountService.RemoveGameAccountAsync(gameAccount);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsGameRunning()
|
||||
{
|
||||
return LaunchExecutionEnsureGameNotRunningHandler.IsGameRunning(out _);
|
||||
|
||||
@@ -10,8 +10,6 @@ namespace Snap.Hutao.Service.Game;
|
||||
|
||||
internal interface IGameServiceFacade
|
||||
{
|
||||
ObservableReorderableDbCollection<GameAccount> GameAccountCollection { get; }
|
||||
|
||||
ValueTask AttachGameAccountToUidAsync(GameAccount gameAccount, string uid);
|
||||
|
||||
ValueTask<GameAccount?> DetectGameAccountAsync(SchemeType scheme);
|
||||
@@ -27,4 +25,6 @@ internal interface IGameServiceFacade
|
||||
ValueTask RemoveGameAccountAsync(GameAccount gameAccount);
|
||||
|
||||
GameAccount? DetectCurrentGameAccount(SchemeType scheme);
|
||||
|
||||
ValueTask<ObservableReorderableDbCollection<GameAccount>> GetGameAccountCollectionAsync();
|
||||
}
|
||||
@@ -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,26 @@ 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);
|
||||
}
|
||||
|
||||
[NotNull]
|
||||
public NameValue<int>? Monitor
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -245,6 +285,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 +326,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
|
||||
{
|
||||
|
||||
@@ -178,9 +178,16 @@ internal sealed partial class PackageConverter
|
||||
|
||||
private async ValueTask<RelativePathVersionItemDictionary> GetLocalItemsAsync(string gameFolder)
|
||||
{
|
||||
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
|
||||
try
|
||||
{
|
||||
return await GetVersionItemsAsync(localSteam).ConfigureAwait(false);
|
||||
using (FileStream localSteam = File.OpenRead(Path.Combine(gameFolder, PackageVersion)))
|
||||
{
|
||||
return await GetVersionItemsAsync(localSteam).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
throw HutaoException.Throw(SH.ServiceGamePackageReadLocalPackageVerionFailed, ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,60 +0,0 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using static Snap.Hutao.Win32.Kernel32;
|
||||
|
||||
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)
|
||||
{
|
||||
int offsetToUserAssembly = IndexOfPattern(localModule.UserAssembly.AsSpan());
|
||||
HutaoException.ThrowIfNot(offsetToUserAssembly >= 0, SH.ServiceGameUnlockerInterestedPatternNotFound);
|
||||
|
||||
nuint rip = localModule.UserAssembly.Address + (uint)offsetToUserAssembly;
|
||||
rip += 5U;
|
||||
rip += (nuint)(*(int*)(rip + 2U) + 6);
|
||||
|
||||
nuint remoteVirtualAddress = remoteModule.UserAssembly.Address + (rip - localModule.UserAssembly.Address);
|
||||
|
||||
nuint ptr = 0;
|
||||
SpinWait.SpinUntil(() => UnsafeReadProcessMemory(context.AllAccess, remoteVirtualAddress, out ptr) && ptr != 0);
|
||||
|
||||
nuint localVirtualAddress = ptr - remoteModule.UnityPlayer.Address + localModule.UnityPlayer.Address;
|
||||
|
||||
while (*(byte*)localVirtualAddress is ASM_CALL or ASM_JMP)
|
||||
{
|
||||
localVirtualAddress += (nuint)(*(int*)(localVirtualAddress + 1) + 5);
|
||||
}
|
||||
|
||||
localVirtualAddress += *(uint*)(localVirtualAddress + 2) + 6;
|
||||
nuint relativeVirtualAddress = localVirtualAddress - localModule.UnityPlayer.Address;
|
||||
context.FpsAddress = remoteModule.UnityPlayer.Address + relativeVirtualAddress;
|
||||
}
|
||||
|
||||
private static int IndexOfPattern(in ReadOnlySpan<byte> memory)
|
||||
{
|
||||
// B9 3C 00 00 00 FF 15
|
||||
ReadOnlySpan<byte> part = [0xB9, 0x3C, 0x00, 0x00, 0x00, 0xFF, 0x15];
|
||||
return memory.IndexOf(part);
|
||||
}
|
||||
|
||||
private static unsafe bool UnsafeReadProcessMemory(HANDLE hProcess, nuint baseAddress, out nuint value)
|
||||
{
|
||||
value = 0;
|
||||
bool result = ReadProcessMemory(hProcess, (void*)baseAddress, ref value, out _);
|
||||
HutaoException.ThrowIfNot(result, SH.ServiceGameUnlockerReadProcessMemoryPointerAddressFailed);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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,34 +45,117 @@ 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);
|
||||
if (view.State is not IslandState.Started)
|
||||
{
|
||||
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)
|
||||
{
|
||||
string gameFoler = gameFileSystem.GameDirectory;
|
||||
string dataFoler = gameFileSystem.DataDirectory;
|
||||
LOAD_LIBRARY_FLAGS flags = LOAD_LIBRARY_FLAGS.LOAD_LIBRARY_AS_IMAGE_RESOURCE;
|
||||
HMODULE unityPlayerAddress = LoadLibraryExW(System.IO.Path.Combine(gameFoler, "UnityPlayer.dll"), default, flags);
|
||||
HMODULE userAssemblyAddress = LoadLibraryExW(System.IO.Path.Combine(dataFoler, "Native", "UserAssembly.dll"), default, flags);
|
||||
IslandEnvironment* pIslandEnvironment = (IslandEnvironment*)handle;
|
||||
pIslandEnvironment->FunctionOffsetFieldOfView = offsets.FunctionOffsetFieldOfView;
|
||||
pIslandEnvironment->FunctionOffsetTargetFrameRate = offsets.FunctionOffsetTargetFrameRate;
|
||||
pIslandEnvironment->FunctionOffsetFog = offsets.FunctionOffsetFog;
|
||||
|
||||
return new(unityPlayerAddress, userAssemblyAddress);
|
||||
UpdateIslandEnvironment(handle, options);
|
||||
}
|
||||
|
||||
private static unsafe IslandEnvironmentView UpdateIslandEnvironment(nint handle, LaunchOptions options)
|
||||
{
|
||||
IslandEnvironment* pIslandEnvironment = (IslandEnvironment*)handle;
|
||||
pIslandEnvironment->FieldOfView = options.TargetFov;
|
||||
pIslandEnvironment->TargetFrameRate = options.TargetFps;
|
||||
pIslandEnvironment->DisableFog = 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 unityPlayerResult = UnsafeFindModule(hProcess, "UnityPlayer.dll", out Module unityPlayer);
|
||||
FindModuleResult userAssemblyResult = UnsafeFindModule(hProcess, "UserAssembly.dll", out Module userAssembly);
|
||||
|
||||
if (unityPlayerResult is FindModuleResult.Ok && userAssemblyResult is FindModuleResult.Ok)
|
||||
{
|
||||
info = new(unityPlayer, userAssembly);
|
||||
return FindModuleResult.Ok;
|
||||
}
|
||||
|
||||
if (unityPlayerResult is FindModuleResult.NoModuleFound && userAssemblyResult is FindModuleResult.NoModuleFound)
|
||||
{
|
||||
info = default;
|
||||
return FindModuleResult.NoModuleFound;
|
||||
}
|
||||
|
||||
info = default;
|
||||
return FindModuleResult.ModuleNotLoaded;
|
||||
}
|
||||
|
||||
private static unsafe FindModuleResult UnsafeFindModule(in HANDLE hProcess, in ReadOnlySpan<char> moduleName, 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)
|
||||
{
|
||||
if (!moduleName.SequenceEqual(MemoryMarshal.CreateReadOnlySpanFromNullTerminated(lpBaseName)))
|
||||
{
|
||||
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,14 @@ namespace Snap.Hutao.Service.Game.Unlocker.Island;
|
||||
|
||||
internal struct IslandEnvironment
|
||||
{
|
||||
public nuint Address;
|
||||
public int Value;
|
||||
public IslandState State;
|
||||
public WIN32_ERROR LastError;
|
||||
public int Reserved;
|
||||
|
||||
public float FieldOfView;
|
||||
public int TargetFrameRate;
|
||||
public bool DisableFog;
|
||||
|
||||
public uint FunctionOffsetFieldOfView;
|
||||
public uint FunctionOffsetTargetFrameRate;
|
||||
public uint FunctionOffsetFog;
|
||||
}
|
||||
@@ -7,9 +7,14 @@ namespace Snap.Hutao.Service.Game.Unlocker.Island;
|
||||
|
||||
internal struct IslandEnvironmentView
|
||||
{
|
||||
public nuint Address;
|
||||
public int Value;
|
||||
public IslandState State;
|
||||
public WIN32_ERROR LastError;
|
||||
public int Reserved;
|
||||
|
||||
public float FieldOfView;
|
||||
public int TargetFrameRate;
|
||||
public bool DisableFog;
|
||||
|
||||
public uint FunctionOffsetFieldOfView;
|
||||
public uint FunctionOffsetTargetFrameRate;
|
||||
public uint 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,48 +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 UnityPlayer;
|
||||
public readonly Module UserAssembly;
|
||||
|
||||
private readonly HMODULE hModuleUnityPlayer;
|
||||
private readonly HMODULE hModuleUserAssembly;
|
||||
|
||||
public RequiredLocalModule(HMODULE unityPlayer, HMODULE userAssembly)
|
||||
{
|
||||
hModuleUnityPlayer = unityPlayer;
|
||||
hModuleUserAssembly = userAssembly;
|
||||
|
||||
// Align the pointer
|
||||
nint unityPlayerMappedView = (nint)(unityPlayer & ~0x3L);
|
||||
nint userAssemblyMappedView = (nint)(userAssembly & ~0x3L);
|
||||
|
||||
HasValue = true;
|
||||
UnityPlayer = new((nuint)unityPlayerMappedView, GetImageSize(unityPlayerMappedView));
|
||||
UserAssembly = new((nuint)userAssemblyMappedView, GetImageSize(userAssemblyMappedView));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
FreeLibrary(hModuleUnityPlayer);
|
||||
FreeLibrary(hModuleUserAssembly);
|
||||
}
|
||||
|
||||
[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,18 +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 UnityPlayer;
|
||||
public readonly Module UserAssembly;
|
||||
|
||||
public RequiredRemoteModule(in Module unityPlayer, in Module userAssembly)
|
||||
{
|
||||
HasValue = true;
|
||||
UnityPlayer = unityPlayer;
|
||||
UserAssembly = userAssembly;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -27,28 +27,20 @@ internal sealed partial class HutaoSpiralAbyssStatisticsCache : IHutaoSpiralAbys
|
||||
private TaskCompletionSource<bool>? wikiAvatarViewModelTaskSource;
|
||||
private TaskCompletionSource<bool>? wikiWeaponViewModelTaskSource;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<AvatarRankView>? AvatarUsageRanks { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<AvatarRankView>? AvatarAppearanceRanks { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<AvatarConstellationInfoView>? AvatarConstellationInfos { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public List<TeamAppearanceView>? TeamAppearances { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Overview? Overview { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Dictionary<AvatarId, AvatarCollocationView>? AvatarCollocations { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Dictionary<WeaponId, WeaponCollocationView>? WeaponCollocations { get; set; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask<bool> InitializeForSpiralAbyssViewAsync()
|
||||
{
|
||||
if (databaseViewModelTaskSource is not null)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataDictionaryIdAvatarWithPlayersSource : IMetadataDictionaryIdAvatarSource
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata.Tower;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataDictionaryIdListTowerLevelSource
|
||||
{
|
||||
Dictionary<TowerLevelGroupId, List<TowerLevel>> IdListTowerLevelMap { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata.Monster;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataDictionaryIdMonsterSource
|
||||
{
|
||||
Dictionary<MonsterRelationshipId, Monster> IdMonsterMap { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata.Tower;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataDictionaryIdTowerFloorSource
|
||||
{
|
||||
Dictionary<TowerFloorId, TowerFloor> IdTowerFloorMap { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata.Tower;
|
||||
using Snap.Hutao.Model.Primitive;
|
||||
|
||||
namespace Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
|
||||
internal interface IMetadataDictionaryIdTowerScheduleSource
|
||||
{
|
||||
Dictionary<TowerScheduleId, TowerSchedule> IdTowerScheduleMap { get; set; }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Model.Metadata.Avatar;
|
||||
using Snap.Hutao.Model.Metadata.Item;
|
||||
using Snap.Hutao.Model.Metadata.Weapon;
|
||||
@@ -64,6 +65,16 @@ internal static class MetadataServiceContextExtension
|
||||
if (context is IMetadataDictionaryIdAvatarSource dictionaryIdAvatarSource)
|
||||
{
|
||||
dictionaryIdAvatarSource.IdAvatarMap = await metadataService.GetIdToAvatarMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
if (context is IMetadataDictionaryIdAvatarWithPlayersSource)
|
||||
{
|
||||
dictionaryIdAvatarSource.IdAvatarMap = AvatarIds.WithPlayers(dictionaryIdAvatarSource.IdAvatarMap);
|
||||
}
|
||||
}
|
||||
|
||||
if (context is IMetadataDictionaryIdListTowerLevelSource dictionaryIdListTowerLevelSource)
|
||||
{
|
||||
dictionaryIdListTowerLevelSource.IdListTowerLevelMap = await metadataService.GetGroupIdToTowerLevelGroupMapAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataDictionaryIdMaterialSource dictionaryIdMaterialSource)
|
||||
@@ -71,6 +82,11 @@ internal static class MetadataServiceContextExtension
|
||||
dictionaryIdMaterialSource.IdMaterialMap = await metadataService.GetIdToMaterialMapAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataDictionaryIdMonsterSource dictionaryIdMonsterSource)
|
||||
{
|
||||
dictionaryIdMonsterSource.IdMonsterMap = await metadataService.GetRelationshipIdToMonsterMapAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataDictionaryIdReliquarySource dictionaryIdReliquarySource)
|
||||
{
|
||||
dictionaryIdReliquarySource.IdReliquaryMap = await metadataService.GetIdToReliquaryMapAsync(token).ConfigureAwait(false);
|
||||
@@ -96,6 +112,16 @@ internal static class MetadataServiceContextExtension
|
||||
dictionaryIdReliquarySubAffixSource.IdReliquarySubAffixMap = await metadataService.GetIdToReliquarySubAffixMapAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataDictionaryIdTowerFloorSource dictionaryIdTowerFloorSource)
|
||||
{
|
||||
dictionaryIdTowerFloorSource.IdTowerFloorMap = await metadataService.GetIdToTowerFloorMapAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataDictionaryIdTowerScheduleSource dictionaryIdTowerScheduleSource)
|
||||
{
|
||||
dictionaryIdTowerScheduleSource.IdTowerScheduleMap = await metadataService.GetIdToTowerScheduleMapAsync(token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (context is IMetadataDictionaryIdWeaponSource dictionaryIdWeaponSource)
|
||||
{
|
||||
dictionaryIdWeaponSource.IdWeaponMap = await metadataService.GetIdToWeaponMapAsync(token).ConfigureAwait(false);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
using Snap.Hutao.Model.Entity;
|
||||
using Snap.Hutao.Model.Metadata;
|
||||
using Snap.Hutao.Service.Metadata;
|
||||
using Snap.Hutao.Service.Metadata.ContextAbstraction;
|
||||
using Snap.Hutao.ViewModel.SpiralAbyss;
|
||||
using Snap.Hutao.ViewModel.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.GameRecord;
|
||||
@@ -34,15 +34,7 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
|
||||
{
|
||||
if (await metadataService.InitializeAsync().ConfigureAwait(false))
|
||||
{
|
||||
// TODO replace to IMetadataContext
|
||||
metadataContext = new()
|
||||
{
|
||||
IdScheduleMap = await metadataService.GetIdToTowerScheduleMapAsync().ConfigureAwait(false),
|
||||
IdFloorMap = await metadataService.GetIdToTowerFloorMapAsync().ConfigureAwait(false),
|
||||
IdLevelGroupMap = await metadataService.GetGroupIdToTowerLevelGroupMapAsync().ConfigureAwait(false),
|
||||
IdMonsterMap = await metadataService.GetRelationshipIdToMonsterMapAsync().ConfigureAwait(false),
|
||||
IdAvatarMap = AvatarIds.WithPlayers(await metadataService.GetIdToAvatarMapAsync().ConfigureAwait(false)),
|
||||
};
|
||||
metadataContext = await metadataService.GetContextAsync<SpiralAbyssMetadataContext>().ConfigureAwait(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -64,7 +56,7 @@ internal sealed partial class SpiralAbyssRecordService : ISpiralAbyssRecordServi
|
||||
Dictionary<uint, SpiralAbyssEntry> entryMap = spiralAbyssRecordDbService.GetSpiralAbyssEntryMapByUid(userAndUid.Uid.Value);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(metadataContext);
|
||||
spiralAbysses = metadataContext.IdScheduleMap.Values
|
||||
spiralAbysses = metadataContext.IdTowerScheduleMap.Values
|
||||
.Select(sch => SpiralAbyssView.From(entryMap.GetValueOrDefault(sch.Id), sch, metadataContext))
|
||||
.OrderByDescending(e => e.ScheduleId)
|
||||
.ToObservableCollection();
|
||||
|
||||
@@ -9,5 +9,5 @@ internal sealed class CheckUpdateResult
|
||||
{
|
||||
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<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.
|
||||
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.Hashing;
|
||||
using Snap.Hutao.Core.IO.Http.Sharding;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Service.Abstraction;
|
||||
using Snap.Hutao.Service.Notification;
|
||||
@@ -11,8 +11,8 @@ using Snap.Hutao.Web.Hutao;
|
||||
using Snap.Hutao.Web.Hutao.Response;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Service.Update;
|
||||
|
||||
@@ -32,7 +32,7 @@ internal sealed partial class UpdateService : IUpdateService
|
||||
await taskContext.SwitchToBackgroundAsync();
|
||||
|
||||
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();
|
||||
|
||||
@@ -44,7 +44,7 @@ internal sealed partial class UpdateService : IUpdateService
|
||||
else
|
||||
{
|
||||
checkUpdateResult.Kind = CheckUpdateResultKind.NeedDownload;
|
||||
checkUpdateResult.HutaoVersionInformation = response.Data;
|
||||
checkUpdateResult.PackageInformation = response.Data;
|
||||
}
|
||||
|
||||
string msixPath = GetUpdatePackagePath();
|
||||
@@ -52,7 +52,7 @@ internal sealed partial class UpdateService : IUpdateService
|
||||
if (!LocalSetting.Get(SettingKeys.OverrideUpdateVersionComparison, false))
|
||||
{
|
||||
// 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))
|
||||
{
|
||||
@@ -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;
|
||||
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(checkUpdateResult.HutaoVersionInformation, GetUpdatePackagePath(), progress, token);
|
||||
return DownloadUpdatePackageAsync(mirrorInformation, GetUpdatePackagePath(), progress, token);
|
||||
}
|
||||
|
||||
public async ValueTask<LaunchUpdaterResult> LaunchUpdaterAsync()
|
||||
public LaunchUpdaterResult LaunchUpdater()
|
||||
{
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
string updaterTargetPath = runtimeOptions.GetDataFolderUpdateCacheFolderFile(UpdaterFilename);
|
||||
|
||||
Uri updaterSourceUri = $"ms-appx:///{UpdaterFilename}".ToUri();
|
||||
StorageFile updaterFile = await StorageFile.GetFileFromApplicationUriAsync(updaterSourceUri);
|
||||
await updaterFile.OverwriteCopyAsync(updaterTargetPath).ConfigureAwait(false);
|
||||
File.Copy(InstalledLocation.GetAbsolutePath(UpdaterFilename), updaterTargetPath, true);
|
||||
|
||||
string commandLine = new CommandLineBuilder()
|
||||
.Append("--package-path", GetUpdatePackagePath(runtimeOptions))
|
||||
@@ -133,37 +130,68 @@ internal sealed partial class UpdateService : IUpdateService
|
||||
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();
|
||||
foreach (string url in versionInformation.Urls)
|
||||
{
|
||||
HttpShardCopyWorkerOptions<UpdateStatus> options = new()
|
||||
case HutaoPackageMirrorType.Direct:
|
||||
using (FileStream fileStream = File.Create(filePath))
|
||||
{
|
||||
HttpClient = httpClient,
|
||||
SourceUrl = url,
|
||||
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);
|
||||
StreamCopyWorker<UpdateStatus> worker = new(webStream, fileStream, bytesRead => new UpdateStatus(version, bytesRead, totalBytes));
|
||||
await worker.CopyAsync(progress).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
string? remoteHash = versionInformation.Sha256;
|
||||
ArgumentNullException.ThrowIfNull(remoteHash);
|
||||
if (await CheckUpdateCacheSHA256Async(filePath, remoteHash, token).ConfigureAwait(false))
|
||||
break;
|
||||
case HutaoPackageMirrorType.Archive:
|
||||
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;
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Snap.Hutao.Core.DependencyInjection.Abstraction;
|
||||
using Snap.Hutao.Model.Entity.Extension;
|
||||
using Snap.Hutao.UI.Xaml.Data;
|
||||
using Snap.Hutao.Web.Hoyolab;
|
||||
using Snap.Hutao.Web.Hoyolab.Bbs.User;
|
||||
using Snap.Hutao.Web.Hoyolab.Passport;
|
||||
@@ -29,7 +30,7 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
|
||||
user.UserInfo = new() { Nickname = SH.ModelBindingUserInitializationFailed };
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
user.UserGameRoles = [];
|
||||
user.UserGameRoles = new([]);
|
||||
}
|
||||
|
||||
return user;
|
||||
@@ -213,8 +214,7 @@ internal sealed partial class UserInitializationService : IUserInitializationSer
|
||||
|
||||
if (userGameRolesResponse.IsOk())
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
user.UserGameRoles = new(userGameRolesResponse.Data.List);
|
||||
user.UserGameRoles = userGameRolesResponse.Data.List.ToAdvancedCollectionView();
|
||||
return user.UserGameRoles.Count > 0;
|
||||
}
|
||||
else
|
||||
|
||||
@@ -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.5">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -6,7 +6,6 @@ using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.Graphics;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
@@ -28,8 +27,8 @@ internal sealed class NotifyIconController : IDisposable
|
||||
{
|
||||
lazyMenu = new(() => new(serviceProvider));
|
||||
|
||||
RuntimeOptions runtimeOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
|
||||
string iconPath = Path.Combine(runtimeOptions.InstalledLocation, "Assets/Logo.ico");
|
||||
string iconPath = InstalledLocation.GetAbsolutePath("Assets/Logo.ico");
|
||||
|
||||
icon = new(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();
|
||||
|
||||
this.SetLayered();
|
||||
this.SetToolWindow();
|
||||
this.SetExStyleLayered();
|
||||
this.SetExStyleToolWindow();
|
||||
|
||||
AppWindow.Title = "SnapHutaoNotifyIconXamlHost";
|
||||
AppWindow.IsShownInSwitchers = false;
|
||||
@@ -54,7 +54,7 @@ internal sealed class NotifyIconXamlHostWindow : Window, IWindowNeedEraseBackgro
|
||||
flyout.ShowAt(Content, new()
|
||||
{
|
||||
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.Graphics.Dwm;
|
||||
using Snap.Hutao.Win32.UI.WindowsAndMessaging;
|
||||
using System.IO;
|
||||
using Windows.Foundation;
|
||||
using Windows.Graphics;
|
||||
using Windows.UI;
|
||||
@@ -58,7 +57,7 @@ internal sealed class XamlWindowController
|
||||
AppOptions appOptions = serviceProvider.GetRequiredService<AppOptions>();
|
||||
|
||||
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
|
||||
if (window is IXamlWindowExtendContentIntoTitleBar xamlWindow)
|
||||
|
||||
@@ -100,7 +100,11 @@ internal sealed class UniformStaggeredLayoutState
|
||||
|
||||
internal void Clear()
|
||||
{
|
||||
RecycleElements();
|
||||
if (items.Count > 0)
|
||||
{
|
||||
RecycleElements();
|
||||
}
|
||||
|
||||
ClearColumns();
|
||||
ClearItems();
|
||||
}
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
using CommunityToolkit.WinUI.Collections;
|
||||
using CommunityToolkit.WinUI.Helpers;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
@@ -28,11 +26,6 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
|
||||
private int deferCounter;
|
||||
private WeakEventListener<AdvancedCollectionView<T>, object?, NotifyCollectionChangedEventArgs>? sourceWeakEventListener;
|
||||
|
||||
public AdvancedCollectionView()
|
||||
: this([])
|
||||
{
|
||||
}
|
||||
|
||||
public AdvancedCollectionView(IList<T> source)
|
||||
{
|
||||
view = [];
|
||||
@@ -628,14 +621,13 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
|
||||
|
||||
if (i < -1 || i >= view.Count)
|
||||
{
|
||||
Debugger.Break(); // Figure out how this will hit.
|
||||
// view is empty, i is 0, current pos is -1
|
||||
OnPropertyChanged(nameof(CurrentItem));
|
||||
return false;
|
||||
}
|
||||
|
||||
CurrentChangingEventArgs e = new();
|
||||
OnCurrentChanging(e);
|
||||
if (e.Cancel)
|
||||
OnCurrentChanging(out bool cancel);
|
||||
if (cancel)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -645,14 +637,17 @@ internal class AdvancedCollectionView<T> : IAdvancedCollectionView<T>, INotifyPr
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnCurrentChanging(CurrentChangingEventArgs e)
|
||||
private void OnCurrentChanging(out bool cancel)
|
||||
{
|
||||
if (!created || deferCounter > 0)
|
||||
{
|
||||
cancel = false;
|
||||
return;
|
||||
}
|
||||
|
||||
CurrentChangingEventArgs e = new();
|
||||
CurrentChanging?.Invoke(this, e);
|
||||
cancel = e.Cancel;
|
||||
}
|
||||
|
||||
private void OnCurrentChanged()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.Data;
|
||||
|
||||
internal static class AdvancedCollectionViewExtension
|
||||
@@ -13,4 +15,11 @@ internal static class AdvancedCollectionViewExtension
|
||||
view.MoveCurrentTo(default);
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static AdvancedCollectionView<T> ToAdvancedCollectionView<T>(this IList<T> source)
|
||||
where T : class, IAdvancedCollectionViewItem
|
||||
{
|
||||
return new AdvancedCollectionView<T>(source);
|
||||
}
|
||||
}
|
||||
@@ -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/>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</cwc:HeaderedContentControl>
|
||||
</cwc:UniformGrid>
|
||||
<RadioButtons
|
||||
Name="ImportModeSelector"
|
||||
x:Name="ImportModeSelector"
|
||||
Grid.Row="1"
|
||||
Margin="0,16,0,0"
|
||||
Header="{shuxm:ResourceString Name=ViewDialogAchievementArchiveImportStrategy}"
|
||||
|
||||
@@ -8,20 +8,11 @@ using Snap.Hutao.Service.Achievement;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// 成就对话框
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("UIAF", typeof(UIAF))]
|
||||
internal sealed partial class AchievementImportDialog : ContentDialog
|
||||
{
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的成就对话框
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="uiaf">uiaf数据</param>
|
||||
public AchievementImportDialog(IServiceProvider serviceProvider, UIAF uiaf)
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -30,10 +21,6 @@ internal sealed partial class AchievementImportDialog : ContentDialog
|
||||
UIAF = uiaf;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取导入选项
|
||||
/// </summary>
|
||||
/// <returns>导入选项</returns>
|
||||
public async ValueTask<ValueResult<bool, ImportStrategyKind>> GetImportStrategyAsync()
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
@@ -73,5 +73,15 @@
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{x:Bind PromotionDelta.Weapon.LevelTarget, Mode=TwoWay}"/>
|
||||
</cwc:SettingsCard>
|
||||
|
||||
<RadioButtons
|
||||
x:Name="SaveModeSelector"
|
||||
Margin="0,13,0,0"
|
||||
Header="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyHeader}"
|
||||
SelectedIndex="0">
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyPreserveExisting}"/>
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting}"/>
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry}"/>
|
||||
</RadioButtons>
|
||||
</StackPanel>
|
||||
</ContentDialog>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
@@ -21,7 +22,7 @@ internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog
|
||||
PromotionDelta = AvatarPromotionDelta.CreateForBaseline();
|
||||
}
|
||||
|
||||
public async ValueTask<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaBaselineAsync()
|
||||
public async ValueTask<ValueResult<bool, CultivatePromotionDeltaOptions>> GetPromotionDeltaBaselineAsync()
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
@@ -50,6 +51,6 @@ internal sealed partial class CultivatePromotionDeltaBatchDialog : ContentDialog
|
||||
LocalSetting.Set(SettingKeys.CultivationWeapon90LevelTarget, weapon.LevelTarget);
|
||||
}
|
||||
|
||||
return new(true, PromotionDelta);
|
||||
return new(true, new(PromotionDelta, (ConsumptionSaveStrategyKind)SaveModeSelector.SelectedIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<DataTemplate x:Key="SkillTemplate">
|
||||
<Border Margin="0,2,0,0" Style="{StaticResource BorderCardStyle}">
|
||||
<Grid Margin="8">
|
||||
<Grid Margin="8" ColumnSpacing="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition Width="160"/>
|
||||
@@ -64,18 +64,17 @@
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ContentDialog.Resources>
|
||||
<Grid>
|
||||
<Grid Margin="0,8,0,0" RowSpacing="6">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
<RowDefinition Height="auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Margin="0,8,0,0"
|
||||
Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
|
||||
<StackPanel Grid.Row="0" Visibility="{x:Bind Avatar, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
|
||||
<Grid
|
||||
Padding="8"
|
||||
ColumnSpacing="6"
|
||||
DataContext="{x:Bind Avatar}"
|
||||
Style="{ThemeResource GridCardStyle}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -123,12 +122,10 @@
|
||||
<ItemsControl ItemTemplate="{StaticResource SkillTemplate}" ItemsSource="{x:Bind Avatar.Skills}"/>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0,6,0,0"
|
||||
Visibility="{x:Bind Weapon, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
|
||||
<StackPanel Grid.Row="1" Visibility="{x:Bind Weapon, Converter={StaticResource EmptyObjectToVisibilityConverter}}">
|
||||
<Grid
|
||||
Padding="8"
|
||||
ColumnSpacing="6"
|
||||
DataContext="{x:Bind Weapon}"
|
||||
Style="{ThemeResource GridCardStyle}">
|
||||
<Grid.ColumnDefinitions>
|
||||
@@ -174,5 +171,15 @@
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
<RadioButtons
|
||||
x:Name="SaveModeSelector"
|
||||
Grid.Row="2"
|
||||
Margin="0,10,0,0"
|
||||
Header="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyHeader}"
|
||||
SelectedIndex="0">
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyPreserveExisting}"/>
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyOverwriteExisting}"/>
|
||||
<RadioButton Content="{shuxm:ResourceString Name=ViewDialogCultivationConsumptionSaveStrategyCreateNewEntry}"/>
|
||||
</RadioButtons>
|
||||
</Grid>
|
||||
</ContentDialog>
|
||||
</ContentDialog>
|
||||
@@ -4,25 +4,17 @@
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Model.Calculable;
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
|
||||
/// <summary>
|
||||
/// 养成计算对话框
|
||||
/// </summary>
|
||||
[HighQuality]
|
||||
[DependencyProperty("Avatar", typeof(ICalculableAvatar))]
|
||||
[DependencyProperty("Weapon", typeof(ICalculableWeapon))]
|
||||
internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
|
||||
{
|
||||
private readonly ITaskContext taskContext;
|
||||
|
||||
/// <summary>
|
||||
/// 构造一个新的养成计算对话框
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">服务提供器</param>
|
||||
/// <param name="options">选项</param>
|
||||
public CultivatePromotionDeltaDialog(IServiceProvider serviceProvider, CalculableOptions options)
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -33,11 +25,7 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
|
||||
Weapon = options.Weapon;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 异步获取提升差异
|
||||
/// </summary>
|
||||
/// <returns>提升差异</returns>
|
||||
public async ValueTask<ValueResult<bool, AvatarPromotionDelta>> GetPromotionDeltaAsync()
|
||||
public async ValueTask<ValueResult<bool, CultivatePromotionDeltaOptions>> GetPromotionDeltaAsync()
|
||||
{
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
ContentDialogResult result = await ShowAsync();
|
||||
@@ -66,6 +54,6 @@ internal sealed partial class CultivatePromotionDeltaDialog : ContentDialog
|
||||
},
|
||||
};
|
||||
|
||||
return new(true, delta);
|
||||
return new(true, new(delta, (ConsumptionSaveStrategyKind)SaveModeSelector.SelectedIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Service.Cultivation;
|
||||
using Snap.Hutao.Web.Hoyolab.Takumi.Event.Calculate;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
|
||||
internal sealed class CultivatePromotionDeltaOptions
|
||||
{
|
||||
public CultivatePromotionDeltaOptions(AvatarPromotionDelta delta, ConsumptionSaveStrategyKind strategy)
|
||||
{
|
||||
Delta = delta;
|
||||
Strategy = strategy;
|
||||
}
|
||||
|
||||
public AvatarPromotionDelta Delta { get; set; }
|
||||
|
||||
public ConsumptionSaveStrategyKind Strategy { get; set; }
|
||||
}
|
||||
@@ -2,17 +2,21 @@
|
||||
x:Class="Snap.Hutao.UI.Xaml.View.Dialog.UpdatePackageDownloadConfirmDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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: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"
|
||||
CloseButtonText="{shuxm:ResourceString Name=ContentDialogCancelCloseButtonText}"
|
||||
DefaultButton="Primary"
|
||||
PrimaryButtonText="{shuxm:ResourceString Name=ContentDialogConfirmPrimaryButtonText}"
|
||||
PrimaryButtonText="{shuxm:ResourceString Name=ViewDialogUpdatePackagePrimaryText}"
|
||||
Style="{ThemeResource DefaultContentDialogStyle}"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel Spacing="16">
|
||||
<TextBlock Text="">
|
||||
<TextBlock>
|
||||
<Run Text="{shuxm:ResourceString Name=ViewTitileUpdatePackageDownloadContent}"/>
|
||||
<Hyperlink NavigateUri="https://hut.ao/statements/update-log.html">
|
||||
<Run Text="{shuxm:ResourceString Name=ViewDialogUpdatePackageDownloadUpdatelogLinkContent}"/>
|
||||
@@ -20,5 +24,16 @@
|
||||
<!-- We leave a Run here to prevent the Hyperlink Stretch -->
|
||||
<Run/>
|
||||
</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>
|
||||
</ContentDialog>
|
||||
@@ -2,13 +2,26 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
|
||||
namespace Snap.Hutao.UI.Xaml.View.Dialog;
|
||||
|
||||
[DependencyProperty("Mirrors", typeof(List<HutaoPackageMirror>))]
|
||||
[DependencyProperty("SelectedItem", typeof(HutaoPackageMirror))]
|
||||
internal sealed partial class UpdatePackageDownloadConfirmDialog : ContentDialog
|
||||
{
|
||||
public UpdatePackageDownloadConfirmDialog()
|
||||
{
|
||||
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"
|
||||
DefaultEasingMode="EaseOut"
|
||||
DefaultEasingType="Cubic"
|
||||
Duration="0:0:0.3">
|
||||
Duration="0:0:0.2">
|
||||
<clw:TransitionConfig Id="Body" ScaleMode="ScaleX"/>
|
||||
<clw:TransitionConfig Id="Header" ScaleMode="ScaleX"/>
|
||||
</clw:TransitionHelper>
|
||||
@@ -71,7 +71,7 @@
|
||||
x:Name="ShowButtonBorder"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
VerticalAlignment="Bottom"
|
||||
clw:TransitionHelper.Id="Body"
|
||||
Background="{ThemeResource SystemControlAcrylicElementBrush}"
|
||||
CornerRadius="{ThemeResource ControlCornerRadius}">
|
||||
@@ -104,17 +104,28 @@
|
||||
x:Name="InfoBarItemsBorder"
|
||||
Margin="16"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
VerticalAlignment="Bottom"
|
||||
clw:TransitionHelper.Id="Body"
|
||||
cw:Effects.Shadow="{ThemeResource CompatCardShadow}"
|
||||
Visibility="Collapsed">
|
||||
<Border Padding="16" Style="{ThemeResource AcrylicBorderCardStyle}">
|
||||
<Grid RowSpacing="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="auto"/>
|
||||
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="auto"/>
|
||||
</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>
|
||||
<ColumnDefinition/>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
@@ -123,7 +134,7 @@
|
||||
VerticalAlignment="Center"
|
||||
clw:TransitionHelper.Id="Header"
|
||||
Style="{StaticResource SubtitleTextBlockStyle}"
|
||||
Text="所有通知"/>
|
||||
Text="{shuxm:ResourceString Name=ViewInfoBarPanelTitle}"/>
|
||||
<StackPanel
|
||||
Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
@@ -164,15 +175,6 @@
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<ScrollView Grid.Row="1">
|
||||
<ItemsControl
|
||||
MaxWidth="480"
|
||||
ItemContainerTransitions="{StaticResource AddDeleteThemeTransitions}"
|
||||
ItemTemplateSelector="{StaticResource InfoBarTemplateSelector}"
|
||||
ItemsPanel="{StaticResource StackPanelSpacing8Template}"
|
||||
ItemsSource="{Binding InfoBars, Mode=OneWay}"/>
|
||||
</ScrollView>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Border>
|
||||
|
||||
@@ -64,7 +64,9 @@
|
||||
Source="{Binding Icon}"/>
|
||||
</shuxcc:VerticalCard.Top>
|
||||
<shuxcc:VerticalCard.Bottom>
|
||||
<TextBlock Text="{Binding Level}"/>
|
||||
<Viewbox MaxWidth="52" StretchDirection="DownOnly">
|
||||
<TextBlock Text="{Binding Level}"/>
|
||||
</Viewbox>
|
||||
</shuxcc:VerticalCard.Bottom>
|
||||
</shuxcc:VerticalCard>
|
||||
</DataTemplate>
|
||||
@@ -143,7 +145,7 @@
|
||||
</shuxcc:VerticalCard>
|
||||
<ItemsControl
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Bottom"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemTemplate="{StaticResource AvatarGridViewSkillTemplate}"
|
||||
ItemsPanel="{StaticResource HorizontalStackPanelSpacing6Template}"
|
||||
ItemsSource="{Binding Skills}"/>
|
||||
@@ -252,7 +254,7 @@
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="auto"/>
|
||||
<ColumnDefinition Width="48"/>
|
||||
<ColumnDefinition Width="100"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<shuxci:CachedImage
|
||||
Grid.Column="0"
|
||||
@@ -266,7 +268,7 @@
|
||||
VerticalAlignment="Center"
|
||||
Foreground="#FFFFFFFF"
|
||||
Style="{StaticResource BaseTextBlockStyle}"
|
||||
Text="{Binding Info.Level}"/>
|
||||
Text="{Binding Level}"/>
|
||||
</Grid>
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -120,6 +120,10 @@
|
||||
<ToggleSwitch IsOn="{Binding AlwaysIsFirstRunAfterUpdate, Mode=TwoWay}"/>
|
||||
</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"/>
|
||||
<cwc:SettingsCard
|
||||
Command="{Binding CompensationGachaLogServiceTimeCommand}"
|
||||
|
||||
@@ -84,7 +84,7 @@ internal static class WindowExtension
|
||||
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);
|
||||
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);
|
||||
nint style = GetWindowLongPtrW(hwnd, WINDOW_LONG_PTR_INDEX.GWL_EXSTYLE);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user