mirror of
https://github.com/wangdage12/Snap.Hutao.git
synced 2026-02-17 15:06:39 +08:00
支持非打包模式直接启动并更改优化设置存储逻辑采用wix 打包 msi 安装包支持脱离沙箱使用,为注入功能开发奠定基础
新增 PackageIdentityAdapter 适配打包和非打包模式,支持通过文件系统路径替代 Windows.Storage.ApplicationData。重构 LocalSetting,抽象为 ISettingStorage 接口,支持 JSON 文件存储非打包模式下的设置。改进异常处理和日志记录,增加调试信息和启动诊断日志。隐藏 AppX 打包配置,更新依赖项版本,支持普通管理员模式下的游戏启动和 DLL 注入。补全 SaltConstants 文件,存储数据签名所需的盐值。 已知问题: 注入功能暂不不支持,缺少服务器提供的当前版本注入信息! 没有启用默认管理员权限打开
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,3 +25,5 @@ src/Snap.Hutao/Snap.Hutao/Generated Files/
|
||||
tools/
|
||||
|
||||
src/Snap.Hutao/Snap.Hutao/AppPackages
|
||||
/src/Snap.Hutao/Snap.Hutao.Installer/obj
|
||||
/src/Snap.Hutao/Snap.Hutao.Installer/bin/x64/Release/en-US
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Fragment>
|
||||
<ComponentGroup Id="ExampleComponents" Directory="INSTALLFOLDER">
|
||||
<Component>
|
||||
<File Source="ExampleComponents.wxs" />
|
||||
</Component>
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
3
src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs
Normal file
3
src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs
Normal file
@@ -0,0 +1,3 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
|
||||
</Wix>
|
||||
8
src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl
Normal file
8
src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl
Normal file
@@ -0,0 +1,8 @@
|
||||
<!--
|
||||
This file contains the declaration of all the localizable strings.
|
||||
-->
|
||||
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
|
||||
|
||||
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
|
||||
|
||||
</WixLocalization>
|
||||
41
src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs
Normal file
41
src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs
Normal file
@@ -0,0 +1,41 @@
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Package Name="Snap.Hutao"
|
||||
Manufacturer="Millennium Science Technology R-D Inst"
|
||||
Version="1.0.0.0"
|
||||
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
|
||||
Scope="perMachine">
|
||||
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<Feature Id="ProductFeature" Title="Snap.Hutao" Level="1">
|
||||
<ComponentGroupRef Id="MainAppComponents" />
|
||||
<ComponentRef Id="ApplicationShortcut" />
|
||||
</Feature>
|
||||
</Package>
|
||||
|
||||
<Fragment>
|
||||
<StandardDirectory Id="ProgramFiles64Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="Snap.Hutao" />
|
||||
</StandardDirectory>
|
||||
|
||||
<StandardDirectory Id="ProgramMenuFolder">
|
||||
<Directory Id="ApplicationProgramsFolder" Name="Snap Hutao" />
|
||||
</StandardDirectory>
|
||||
</Fragment>
|
||||
|
||||
<Fragment>
|
||||
<Component Id="ApplicationShortcut" Directory="ApplicationProgramsFolder" Guid="*">
|
||||
|
||||
<Shortcut Id="ApplicationStartMenuShortcut"
|
||||
Name="Snap Hutao"
|
||||
Description="Snap Hutao Client"
|
||||
Target="[INSTALLFOLDER]Snap.Hutao.exe"
|
||||
WorkingDirectory="INSTALLFOLDER" />
|
||||
|
||||
<RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall" />
|
||||
|
||||
<RegistryValue Root="HKCU" Key="Software\Snap.Hutao" Name="installed" Type="integer" Value="1" KeyPath="yes" />
|
||||
</Component>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
@@ -0,0 +1,22 @@
|
||||
<Project Sdk="WixToolset.Sdk/6.0.2">
|
||||
<PropertyGroup>
|
||||
<SuppressIces>ICE03;ICE60</SuppressIces>
|
||||
<Platform>x64</Platform>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Snap.Hutao\Snap.Hutao.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<HarvestDirectory Include="..\Snap.Hutao\bin\x64\Release\net10.0-windows10.0.26100.0\win-x64">
|
||||
<ComponentGroupName>MainAppComponents</ComponentGroupName>
|
||||
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
|
||||
<SuppressCom>true</SuppressCom>
|
||||
<SuppressRegistry>true</SuppressRegistry>
|
||||
<SuppressRootDirectory>true</SuppressRootDirectory>
|
||||
</HarvestDirectory>
|
||||
|
||||
<PackageReference Include="WixToolset.Heat" Version="4.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,24 +1,24 @@
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<Platform Name="Any CPU" />
|
||||
<Platform Name="ARM64" />
|
||||
<Platform Name="x64" />
|
||||
<Platform Name="x86" />
|
||||
</Configurations>
|
||||
<Folder Name="/Solution Items/">
|
||||
<File Path=".editorconfig" />
|
||||
<File Path=".vsconfig" />
|
||||
</Folder>
|
||||
<Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj" />
|
||||
<Project Path="Snap.Hutao\Snap.Hutao.csproj">
|
||||
<!-- For Rider -->
|
||||
<Configuration Solution="Debug|Any CPU" Project="Debug|x64|Deploy" />
|
||||
<Configuration Solution="Debug|x64" Project="Debug|x64|Deploy" />
|
||||
<Configuration Solution="Release|Any CPU" Project="Release|x64|Deploy" />
|
||||
<Configuration Solution="Release|x64" Project="Release|x64|Deploy" />
|
||||
<!-- For Visual Studio -->
|
||||
<Project Path="Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj" Type="b7dd6f7e-def8-4e67-b5b7-07ef123db6f0" Id="91a04cd0-28cc-4562-92e1-202bc163edd7">
|
||||
<Platform Solution="*|Any CPU" Project="x64" />
|
||||
<Platform Solution="*|arm64" Project="arm64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
<Platform Solution="*|x86" Project="x86" />
|
||||
<Deploy />
|
||||
</Project>
|
||||
<Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj">
|
||||
<Build Solution="*|ARM64" Project="false" />
|
||||
<Build Solution="*|x86" Project="false" />
|
||||
</Project>
|
||||
<Project Path="Snap.Hutao\Snap.Hutao.csproj">
|
||||
<Platform Project="x64" />
|
||||
<Deploy Solution="*|Any CPU" />
|
||||
<Deploy Solution="*|x64" />
|
||||
</Project>
|
||||
</Solution>
|
||||
@@ -13,6 +13,7 @@ using Snap.Hutao.Service;
|
||||
using Snap.Hutao.UI.Xaml;
|
||||
using Snap.Hutao.UI.Xaml.Control.Theme;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao;
|
||||
|
||||
@@ -64,6 +65,11 @@ public sealed partial class App : Application
|
||||
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
// ⚠️ 添加启动诊断
|
||||
#if DEBUG
|
||||
Core.ApplicationModel.PackageIdentityDiagnostics.LogDiagnostics();
|
||||
#endif
|
||||
|
||||
DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT();
|
||||
|
||||
try
|
||||
@@ -71,17 +77,40 @@ public sealed partial class App : Application
|
||||
// Important: You must call AppNotificationManager::Default().Register
|
||||
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
|
||||
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
|
||||
AppNotificationManager.Default.Register();
|
||||
|
||||
try
|
||||
{
|
||||
AppNotificationManager.Default.Register();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// In unpackaged mode, this might fail - continue anyway
|
||||
}
|
||||
|
||||
// E_INVALIDARG E_OUTOFMEMORY
|
||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
AppActivationArguments? activatedEventArgs = null;
|
||||
PrivateNamedPipeClient? namedPipeClient = null;
|
||||
|
||||
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
|
||||
try
|
||||
{
|
||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao"));
|
||||
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
||||
Exit();
|
||||
return;
|
||||
activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||
namedPipeClient = serviceProvider.GetRequiredService<PrivateNamedPipeClient>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// In unpackaged mode, AppInstance might not work
|
||||
// Create a default activation argument for launch
|
||||
}
|
||||
|
||||
if (activatedEventArgs is not null && namedPipeClient is not null)
|
||||
{
|
||||
if (namedPipeClient.TryRedirectActivationTo(activatedEventArgs))
|
||||
{
|
||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao"));
|
||||
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
||||
Exit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation($"{ConsoleBanner}");
|
||||
@@ -90,10 +119,30 @@ public sealed partial class App : Application
|
||||
|
||||
// Manually invoke
|
||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Activate and Initialize", "Application"));
|
||||
activation.ActivateAndInitialize(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
|
||||
|
||||
HutaoActivationArguments hutaoArgs = activatedEventArgs is not null
|
||||
? HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs)
|
||||
: HutaoActivationArguments.CreateDefaultLaunchArguments();
|
||||
|
||||
activation.ActivateAndInitialize(hutaoArgs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// ⚠️ 添加更详细的异常日志
|
||||
try
|
||||
{
|
||||
string errorPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Hutao",
|
||||
"startup_error.txt");
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(errorPath)!);
|
||||
File.WriteAllText(errorPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Startup Error:\n{ex}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore
|
||||
}
|
||||
|
||||
SentrySdk.CaptureException(ex);
|
||||
SentrySdk.Flush();
|
||||
|
||||
|
||||
@@ -31,8 +31,15 @@ public static partial class Bootstrap
|
||||
[STAThread]
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Starting...");
|
||||
#endif
|
||||
|
||||
if (Mutex.TryOpenExisting(LockName, out _))
|
||||
{
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Another instance is running");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,9 +49,16 @@ public static partial class Bootstrap
|
||||
mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow));
|
||||
mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity);
|
||||
Debug.Assert(created);
|
||||
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Mutex created");
|
||||
#endif
|
||||
}
|
||||
catch (WaitHandleCannotBeOpenedException)
|
||||
{
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] WaitHandleCannotBeOpenedException");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -54,34 +68,70 @@ public static partial class Bootstrap
|
||||
{
|
||||
if (!OSPlatformSupported())
|
||||
{
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] OS not supported");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Setting environment variables");
|
||||
#endif
|
||||
|
||||
Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000");
|
||||
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128");
|
||||
AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false);
|
||||
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Initializing COM wrappers");
|
||||
#endif
|
||||
|
||||
ComWrappersSupport.InitializeComWrappers();
|
||||
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Initializing DI container");
|
||||
#endif
|
||||
|
||||
// By adding the using statement, we can dispose the injected services when closing
|
||||
using (ServiceProvider serviceProvider = DependencyInjection.Initialize())
|
||||
{
|
||||
Thread.CurrentThread.Name = "Snap Hutao Application Main Thread";
|
||||
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Calling Application.Start()");
|
||||
#endif
|
||||
|
||||
// If you hit a COMException REGDB_E_CLASSNOTREG (0x80040154) during debugging
|
||||
// You can delete bin and obj folder and then rebuild.
|
||||
// In a Desktop app this runs a message pump internally,
|
||||
// and does not return until the application shuts down.
|
||||
Application.Start(AppInitializationCallback);
|
||||
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Application.Start() returned");
|
||||
#endif
|
||||
|
||||
XamlApplicationLifetime.Exited = true;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Flushing Sentry");
|
||||
#endif
|
||||
|
||||
SentrySdk.Flush();
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Exiting");
|
||||
#endif
|
||||
}
|
||||
|
||||
private static void InitializeApp(ApplicationInitializationCallbackParams param)
|
||||
{
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Callback invoked");
|
||||
#endif
|
||||
|
||||
Gen2GcCallback.Register(() =>
|
||||
{
|
||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime"));
|
||||
@@ -90,8 +140,17 @@ public static partial class Bootstrap
|
||||
|
||||
IServiceProvider serviceProvider = Ioc.Default;
|
||||
|
||||
_ = serviceProvider.GetRequiredService<ITaskContext>();
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Creating App instance");
|
||||
#endif
|
||||
|
||||
// ⚠️ 只创建 App
|
||||
// TaskContext 将在第一次被需要时自动创建(延迟初始化)
|
||||
_ = serviceProvider.GetRequiredService<App>();
|
||||
|
||||
#if DEBUG
|
||||
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Initialization complete (TaskContext will be lazily created)");
|
||||
#endif
|
||||
}
|
||||
|
||||
private static bool OSPlatformSupported()
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
using System.Collections.Frozen;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace Snap.Hutao.Core.ApplicationModel;
|
||||
|
||||
internal static class LimitedAccessFeatures
|
||||
{
|
||||
private static readonly string PackagePublisherId = Package.Current.Id.PublisherId;
|
||||
private static readonly string PackageFamilyName = Package.Current.Id.FamilyName;
|
||||
private static readonly string PackagePublisherId = PackageIdentityAdapter.PublisherId;
|
||||
private static readonly string PackageFamilyName = PackageIdentityAdapter.FamilyName;
|
||||
|
||||
private static readonly FrozenDictionary<string, string> Features = WinRTAdaptive.ToFrozenDictionary(
|
||||
[
|
||||
@@ -67,8 +66,15 @@ internal static class LimitedAccessFeatures
|
||||
KeyValuePair.Create("com.microsoft.windows.windowdecorations", "425261a8-7f73-4319-8a53-fc13f87e1717")
|
||||
]);
|
||||
|
||||
public static LimitedAccessFeatureRequestResult TryUnlockFeature(string featureId)
|
||||
public static Windows.ApplicationModel.LimitedAccessFeatureRequestResult TryUnlockFeature(string featureId)
|
||||
{
|
||||
if (!PackageIdentityAdapter.HasPackageIdentity)
|
||||
{
|
||||
// In unpackaged mode, we can't unlock limited access features
|
||||
// Create a dummy result - actual implementation will handle the failure
|
||||
return default;
|
||||
}
|
||||
|
||||
return Windows.ApplicationModel.LimitedAccessFeatures.TryUnlockFeature(featureId, GetToken(featureId), GetAttestation(featureId));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Snap.Hutao.Core.ApplicationModel;
|
||||
|
||||
/// <summary>
|
||||
/// Adapter to handle both packaged and unpackaged app scenarios
|
||||
/// </summary>
|
||||
internal static class PackageIdentityAdapter
|
||||
{
|
||||
private static readonly Lazy<bool> LazyHasPackageIdentity = new(CheckPackageIdentity);
|
||||
private static readonly Lazy<string> LazyAppDirectory = new(GetAppDirectoryPath);
|
||||
private static readonly Lazy<Version> LazyAppVersion = new(GetAppVersionInternal);
|
||||
private static readonly Lazy<string> LazyFamilyName = new(GetFamilyNameInternal);
|
||||
private static readonly Lazy<string> LazyPublisherId = new(GetPublisherIdInternal);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the app has package identity
|
||||
/// </summary>
|
||||
public static bool HasPackageIdentity => LazyHasPackageIdentity.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Get application installation directory
|
||||
/// </summary>
|
||||
public static string AppDirectory => LazyAppDirectory.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Get application version
|
||||
/// </summary>
|
||||
public static Version AppVersion => LazyAppVersion.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Get package family name (or fallback for unpackaged)
|
||||
/// </summary>
|
||||
public static string FamilyName => LazyFamilyName.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Get publisher ID (or fallback for unpackaged)
|
||||
/// </summary>
|
||||
public static string PublisherId => LazyPublisherId.Value;
|
||||
|
||||
private static bool CheckPackageIdentity()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Try to access Package.Current - if it throws, we don't have package identity
|
||||
_ = Windows.ApplicationModel.Package.Current.Id;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetAppDirectoryPath()
|
||||
{
|
||||
if (HasPackageIdentity)
|
||||
{
|
||||
return Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
|
||||
}
|
||||
|
||||
// Unpackaged: use the exe directory
|
||||
string? exePath = Process.GetCurrentProcess().MainModule?.FileName;
|
||||
ArgumentException.ThrowIfNullOrEmpty(exePath);
|
||||
string? directory = Path.GetDirectoryName(exePath);
|
||||
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static Version GetAppVersionInternal()
|
||||
{
|
||||
if (HasPackageIdentity)
|
||||
{
|
||||
return Windows.ApplicationModel.Package.Current.Id.Version.ToVersion();
|
||||
}
|
||||
|
||||
// Unpackaged: use assembly version
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
Version? version = assembly.GetName().Version;
|
||||
return version ?? new Version(1, 0, 0, 0);
|
||||
}
|
||||
|
||||
private static string GetFamilyNameInternal()
|
||||
{
|
||||
if (HasPackageIdentity)
|
||||
{
|
||||
return Windows.ApplicationModel.Package.Current.Id.FamilyName;
|
||||
}
|
||||
|
||||
// Unpackaged: use a deterministic fallback
|
||||
return "Snap.Hutao.Unpackaged";
|
||||
}
|
||||
|
||||
private static string GetPublisherIdInternal()
|
||||
{
|
||||
if (HasPackageIdentity)
|
||||
{
|
||||
return Windows.ApplicationModel.Package.Current.Id.PublisherId;
|
||||
}
|
||||
|
||||
// Unpackaged: use a fallback
|
||||
return "CN=DGPStudio";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
namespace Snap.Hutao.Core.ApplicationModel;
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic helper for PackageIdentityAdapter
|
||||
/// </summary>
|
||||
internal static class PackageIdentityDiagnostics
|
||||
{
|
||||
public static void LogDiagnostics()
|
||||
{
|
||||
try
|
||||
{
|
||||
string logPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"Hutao",
|
||||
"startup_diagnostics.txt");
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(logPath)!);
|
||||
|
||||
using (StreamWriter writer = File.CreateText(logPath))
|
||||
{
|
||||
writer.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Startup Diagnostics");
|
||||
writer.WriteLine($"HasPackageIdentity: {PackageIdentityAdapter.HasPackageIdentity}");
|
||||
writer.WriteLine($"AppVersion: {PackageIdentityAdapter.AppVersion}");
|
||||
writer.WriteLine($"AppDirectory: {PackageIdentityAdapter.AppDirectory}");
|
||||
writer.WriteLine($"FamilyName: {PackageIdentityAdapter.FamilyName}");
|
||||
writer.WriteLine($"PublisherId: {PackageIdentityAdapter.PublisherId}");
|
||||
writer.WriteLine("---");
|
||||
}
|
||||
|
||||
Debug.WriteLine($"Diagnostics written to: {logPath}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to write diagnostics: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Snap.Hutao.Core.ApplicationModel;
|
||||
using Snap.Hutao.Model.Entity.Database;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
@@ -17,7 +18,20 @@ internal sealed partial class HutaoDiagnostics : IHutaoDiagnostics
|
||||
[GeneratedConstructor]
|
||||
public partial HutaoDiagnostics(IServiceProvider serviceProvider);
|
||||
|
||||
public ApplicationDataContainer LocalSettings { get => ApplicationData.Current.LocalSettings; }
|
||||
public ApplicationDataContainer? LocalSettings
|
||||
{
|
||||
get
|
||||
{
|
||||
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||
{
|
||||
return ApplicationData.Current.LocalSettings;
|
||||
}
|
||||
|
||||
// In unpackaged mode, ApplicationDataContainer is not available
|
||||
// Return null - scripting/diagnostics code should handle this gracefully
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask<int> ExecuteSqlAsync(string sql)
|
||||
{
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Diagnostics;
|
||||
[SuppressMessage("", "SH001", Justification = "IHutaoDiagnostics must be public in order to be exposed to the scripting environment")]
|
||||
public interface IHutaoDiagnostics
|
||||
{
|
||||
ApplicationDataContainer LocalSettings { get; }
|
||||
ApplicationDataContainer? LocalSettings { get; }
|
||||
|
||||
ValueTask<int> ExecuteSqlAsync(string sql);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
using Microsoft.Win32;
|
||||
using Microsoft.Windows.AppNotifications;
|
||||
using Snap.Hutao.Core.ApplicationModel;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Core.IO;
|
||||
using Snap.Hutao.Core.IO.Hashing;
|
||||
@@ -13,31 +14,32 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
internal static class HutaoRuntime
|
||||
{
|
||||
public static Version Version { get; } = Package.Current.Id.Version.ToVersion();
|
||||
public static Version Version { get; } = PackageIdentityAdapter.AppVersion;
|
||||
|
||||
public static string UserAgent { get; } = $"Snap Hutao/{Version}";
|
||||
|
||||
public static string DataDirectory { get; } = InitializeDataDirectory();
|
||||
|
||||
public static string LocalCacheDirectory { get; } = ApplicationData.Current.LocalCacheFolder.Path;
|
||||
public static string LocalCacheDirectory { get; } = InitializeLocalCacheDirectory();
|
||||
|
||||
public static string FamilyName { get; } = Package.Current.Id.FamilyName;
|
||||
public static string FamilyName { get; } = PackageIdentityAdapter.FamilyName;
|
||||
|
||||
public static string DeviceId { get; } = InitializeDeviceId();
|
||||
|
||||
public static WebView2Version WebView2Version { get; } = InitializeWebView2();
|
||||
|
||||
public static bool IsProcessElevated { get; } = LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false) || Environment.IsPrivilegedProcess;
|
||||
// ⚠️ 延迟初始化以避免循环依赖
|
||||
private static readonly Lazy<bool> LazyIsProcessElevated = new(GetIsProcessElevated);
|
||||
|
||||
public static bool IsProcessElevated => LazyIsProcessElevated.Value;
|
||||
|
||||
// Requires main thread
|
||||
public static bool IsAppNotificationEnabled { get; } = AppNotificationManager.Default.Setting is AppNotificationSetting.Enabled;
|
||||
public static bool IsAppNotificationEnabled { get; } = CheckAppNotificationEnabled();
|
||||
|
||||
public static string? GetDisplayName()
|
||||
{
|
||||
@@ -106,32 +108,57 @@ internal static class HutaoRuntime
|
||||
return string.Intern(directory);
|
||||
}
|
||||
|
||||
private static string InitializeDataDirectory()
|
||||
private static bool GetIsProcessElevated()
|
||||
{
|
||||
// Delete the previous data folder if it exists
|
||||
// ⚠️ 这里调用 LocalSetting 时,确保 DataDirectory 已经初始化完成
|
||||
try
|
||||
{
|
||||
string previousDirectory = LocalSetting.Get(SettingKeys.PreviousDataDirectoryToDelete, string.Empty);
|
||||
if (!string.IsNullOrEmpty(previousDirectory) && Directory.Exists(previousDirectory))
|
||||
{
|
||||
Directory.SetReadOnly(previousDirectory, false);
|
||||
Directory.Delete(previousDirectory, true);
|
||||
}
|
||||
return LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false) || Environment.IsPrivilegedProcess;
|
||||
}
|
||||
finally
|
||||
catch
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.PreviousDataDirectoryToDelete, string.Empty);
|
||||
// 如果读取失败,使用默认值
|
||||
return Environment.IsPrivilegedProcess;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the preferred path is set
|
||||
string currentDirectory = LocalSetting.Get(SettingKeys.DataDirectory, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(currentDirectory))
|
||||
private static string InitializeLocalCacheDirectory()
|
||||
{
|
||||
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||
{
|
||||
Directory.CreateDirectory(currentDirectory);
|
||||
return currentDirectory;
|
||||
return Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path;
|
||||
}
|
||||
|
||||
// Unpackaged: use %LOCALAPPDATA%\Snap.Hutao\Cache
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
const string FolderName
|
||||
#if IS_ALPHA_BUILD
|
||||
= "HutaoAlpha";
|
||||
#elif IS_CANARY_BUILD
|
||||
= "HutaoCanary";
|
||||
#else
|
||||
= "Hutao";
|
||||
#endif
|
||||
string cacheDir = Path.Combine(localAppData, FolderName, "Cache");
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
return cacheDir;
|
||||
}
|
||||
|
||||
private static bool CheckAppNotificationEnabled()
|
||||
{
|
||||
try
|
||||
{
|
||||
return AppNotificationManager.Default.Setting is AppNotificationSetting.Enabled;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// In unpackaged mode, this might fail - return false
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static string InitializeDataDirectory()
|
||||
{
|
||||
const string FolderName
|
||||
#if IS_ALPHA_BUILD
|
||||
= "HutaoAlpha";
|
||||
@@ -141,30 +168,43 @@ internal static class HutaoRuntime
|
||||
= "Hutao";
|
||||
#endif
|
||||
|
||||
// ⚠️ 不要在这里调用 LocalSetting - 会导致循环依赖
|
||||
// 先确定默认的数据目录位置
|
||||
|
||||
// Check if the old documents path exists
|
||||
string myDocumentsHutaoDirectory = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FolderName));
|
||||
if (Directory.Exists(myDocumentsHutaoDirectory))
|
||||
{
|
||||
LocalSetting.Set(SettingKeys.DataDirectory, myDocumentsHutaoDirectory);
|
||||
return myDocumentsHutaoDirectory;
|
||||
}
|
||||
|
||||
// Prefer LocalApplicationData
|
||||
string localApplicationData = ApplicationData.Current.LocalFolder.Path;
|
||||
string path = Path.GetFullPath(Path.Combine(localApplicationData, FolderName));
|
||||
// Use LocalApplicationData
|
||||
string localApplicationData;
|
||||
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||
{
|
||||
localApplicationData = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unpackaged: use %LOCALAPPDATA%
|
||||
localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
}
|
||||
|
||||
string defaultPath = Path.GetFullPath(Path.Combine(localApplicationData, FolderName));
|
||||
|
||||
// ⚠️ 延迟处理:在第一次使用 LocalSetting 后再检查是否有自定义路径
|
||||
// 这里返回默认路径,后续通过 LocalSetting 可能会更新
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(path);
|
||||
Directory.CreateDirectory(defaultPath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// FileNotFoundException | UnauthorizedAccessException
|
||||
// We don't have enough permission
|
||||
HutaoException.InvalidOperation($"Failed to create data folder: {path}", ex);
|
||||
HutaoException.InvalidOperation($"Failed to create data folder: {defaultPath}", ex);
|
||||
}
|
||||
|
||||
LocalSetting.Set(SettingKeys.DataDirectory, path);
|
||||
return path;
|
||||
return defaultPath;
|
||||
}
|
||||
|
||||
private static string InitializeDeviceId()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ApplicationModel;
|
||||
using System.IO;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
using Windows.ApplicationModel;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Core;
|
||||
|
||||
@@ -13,7 +12,7 @@ internal static class InstalledLocation
|
||||
{
|
||||
public static string GetAbsolutePath(string relativePath)
|
||||
{
|
||||
return Path.Combine(Package.Current.InstalledLocation.Path, relativePath);
|
||||
return Path.Combine(PackageIdentityAdapter.AppDirectory, relativePath);
|
||||
}
|
||||
|
||||
public static void CopyFileFromApplicationUri(string url, string path)
|
||||
@@ -23,8 +22,26 @@ internal static class InstalledLocation
|
||||
static async Task CopyApplicationUriFileCoreAsync(string url, string path)
|
||||
{
|
||||
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
||||
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(url.ToUri());
|
||||
using (Stream outputStream = (await file.OpenReadAsync()).AsStreamForRead())
|
||||
|
||||
Uri uri = url.ToUri();
|
||||
Stream outputStream;
|
||||
|
||||
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||
{
|
||||
// Packaged: use StorageFile
|
||||
Windows.Storage.StorageFile file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(uri);
|
||||
outputStream = (await file.OpenReadAsync()).AsStreamForRead();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unpackaged: read from file system directly
|
||||
// Assume ms-appx:/// points to the app directory
|
||||
string localPath = uri.LocalPath.TrimStart('/');
|
||||
string fullPath = Path.Combine(PackageIdentityAdapter.AppDirectory, localPath);
|
||||
outputStream = File.OpenRead(fullPath);
|
||||
}
|
||||
|
||||
using (outputStream)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
|
||||
@@ -74,8 +74,15 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
||||
|
||||
public void ActivateAndInitialize(HutaoActivationArguments args)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation] ActivateAndInitialize called");
|
||||
#endif
|
||||
|
||||
if (Volatile.Read(ref isActivating) is 1)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation] Already activating, returning");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -85,21 +92,52 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
||||
{
|
||||
try
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation] Starting activation process");
|
||||
#endif
|
||||
|
||||
using (await activateLock.LockAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref isActivating, 1, 0) is not 0)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation] Race condition detected, returning");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleActivationAsync");
|
||||
#endif
|
||||
|
||||
await UnsynchronizedHandleActivationAsync(args).ConfigureAwait(false);
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleInitializationAsync");
|
||||
#endif
|
||||
|
||||
await UnsynchronizedHandleInitializationAsync().ConfigureAwait(false);
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation] Initialization completed successfully");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[AppActivation] Exception during activation: {ex}");
|
||||
#endif
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
||||
Interlocked.Exchange(ref isActivating, 0);
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation] ActivationAndInitializationCompleted set to true");
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,16 +351,36 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
||||
private async ValueTask<Window?> WaitWindowAsync<TWindow>()
|
||||
where TWindow : Window
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Waiting for window type: {typeof(TWindow).Name}");
|
||||
#endif
|
||||
|
||||
await taskContext.SwitchToMainThreadAsync();
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation.WaitWindowAsync] Switched to main thread");
|
||||
#endif
|
||||
|
||||
if (currentXamlWindowReference.Window is not { } window)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation.WaitWindowAsync] Creating new window instance");
|
||||
#endif
|
||||
|
||||
try
|
||||
{
|
||||
window = serviceProvider.GetRequiredService<TWindow>();
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Window created successfully: {window.GetType().Name}");
|
||||
#endif
|
||||
}
|
||||
catch (COMException)
|
||||
catch (COMException ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[AppActivation.WaitWindowAsync] COMException: {ex}");
|
||||
#endif
|
||||
|
||||
if (XamlApplicationLifetime.Exiting)
|
||||
{
|
||||
return default;
|
||||
@@ -330,11 +388,33 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Exception creating window: {ex}");
|
||||
#endif
|
||||
throw;
|
||||
}
|
||||
|
||||
currentXamlWindowReference.Window = window;
|
||||
}
|
||||
else
|
||||
{
|
||||
#if DEBUG
|
||||
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Using existing window: {window.GetType().Name}");
|
||||
#endif
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation.WaitWindowAsync] Calling window.SwitchTo()");
|
||||
#endif
|
||||
|
||||
window.SwitchTo();
|
||||
|
||||
#if DEBUG
|
||||
Debug.WriteLine("[AppActivation.WaitWindowAsync] Window activated");
|
||||
#endif
|
||||
|
||||
window.AppWindow?.MoveInZOrderAtTop();
|
||||
return window;
|
||||
}
|
||||
|
||||
@@ -66,4 +66,14 @@ internal sealed class HutaoActivationArguments
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static HutaoActivationArguments CreateDefaultLaunchArguments()
|
||||
{
|
||||
return new HutaoActivationArguments
|
||||
{
|
||||
IsRedirectTo = false,
|
||||
Kind = HutaoActivationKind.Launch,
|
||||
LaunchActivatedArguments = string.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.ApplicationModel;
|
||||
using Snap.Hutao.Core.ExceptionService;
|
||||
using Snap.Hutao.Factory.Process;
|
||||
using Snap.Hutao.Win32;
|
||||
using Snap.Hutao.Win32.Foundation;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Frozen;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Core.Setting;
|
||||
@@ -36,40 +40,20 @@ internal static class LocalSetting
|
||||
typeof(ApplicationDataCompositeValue)
|
||||
];
|
||||
|
||||
private static readonly ApplicationDataContainer Container = ApplicationData.Current.LocalSettings;
|
||||
private static readonly Lazy<ISettingStorage> LazyStorage = new(CreateStorage);
|
||||
|
||||
private static ISettingStorage Storage => LazyStorage.Value;
|
||||
|
||||
public static T Get<T>(string key, T defaultValue = default!)
|
||||
{
|
||||
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
||||
if (Container.Values.TryGetValue(key, out object? value))
|
||||
{
|
||||
// unbox the value
|
||||
return value is null ? defaultValue : (T)value;
|
||||
}
|
||||
|
||||
Set(key, defaultValue);
|
||||
return defaultValue;
|
||||
return Storage.Get(key, defaultValue);
|
||||
}
|
||||
|
||||
public static void Set<T>(string key, T value)
|
||||
{
|
||||
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
||||
|
||||
try
|
||||
{
|
||||
Container.Values[key] = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 状态管理器无法写入设置
|
||||
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_STATE_WRITE_SETTING_FAILED))
|
||||
{
|
||||
HutaoNative.Instance.ShowErrorMessage(ex.Message, ExceptionFormat.Format(ex));
|
||||
ProcessFactory.KillCurrent();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
Storage.Set(key, value);
|
||||
}
|
||||
|
||||
public static void SetIf<T>(bool condition, string key, T value)
|
||||
@@ -103,4 +87,299 @@ internal static class LocalSetting
|
||||
Set(key, newValue);
|
||||
return oldValue;
|
||||
}
|
||||
|
||||
private static ISettingStorage CreateStorage()
|
||||
{
|
||||
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||
{
|
||||
return new PackagedSettingStorage();
|
||||
}
|
||||
|
||||
return new UnpackagedSettingStorage();
|
||||
}
|
||||
|
||||
private interface ISettingStorage
|
||||
{
|
||||
T Get<T>(string key, T defaultValue);
|
||||
void Set<T>(string key, T value);
|
||||
}
|
||||
|
||||
private sealed class PackagedSettingStorage : ISettingStorage
|
||||
{
|
||||
private readonly ApplicationDataContainer container = ApplicationData.Current.LocalSettings;
|
||||
|
||||
public T Get<T>(string key, T defaultValue)
|
||||
{
|
||||
if (container.Values.TryGetValue(key, out object? value))
|
||||
{
|
||||
// unbox the value
|
||||
return value is null ? defaultValue : (T)value;
|
||||
}
|
||||
|
||||
Set(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void Set<T>(string key, T value)
|
||||
{
|
||||
try
|
||||
{
|
||||
container.Values[key] = value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 状态管理器无法写入设置
|
||||
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_STATE_WRITE_SETTING_FAILED))
|
||||
{
|
||||
HutaoNative.Instance.ShowErrorMessage(ex.Message, ExceptionFormat.Format(ex));
|
||||
ProcessFactory.KillCurrent();
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class UnpackagedSettingStorage : ISettingStorage
|
||||
{
|
||||
private readonly string settingsFilePath;
|
||||
private readonly ConcurrentDictionary<string, object?> cache = new();
|
||||
private readonly object fileLock = new();
|
||||
private readonly JsonSerializerOptions jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
Converters =
|
||||
{
|
||||
new ApplicationDataCompositeValueJsonConverter(),
|
||||
}
|
||||
};
|
||||
|
||||
public UnpackagedSettingStorage()
|
||||
{
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
const string FolderName
|
||||
#if IS_ALPHA_BUILD
|
||||
= "HutaoAlpha";
|
||||
#elif IS_CANARY_BUILD
|
||||
= "HutaoCanary";
|
||||
#else
|
||||
= "Hutao";
|
||||
#endif
|
||||
string settingsDir = Path.Combine(localAppData, FolderName, "Settings");
|
||||
Directory.CreateDirectory(settingsDir);
|
||||
settingsFilePath = Path.Combine(settingsDir, "LocalSettings.json");
|
||||
|
||||
LoadFromFile();
|
||||
}
|
||||
|
||||
public T Get<T>(string key, T defaultValue)
|
||||
{
|
||||
if (cache.TryGetValue(key, out object? value))
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Handle JSON deserialization for complex types
|
||||
if (value is JsonElement jsonElement)
|
||||
{
|
||||
try
|
||||
{
|
||||
// ⚠️ 特殊处理:JSON 数字类型转换
|
||||
Type targetType = typeof(T);
|
||||
|
||||
if (jsonElement.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
if (targetType == typeof(int))
|
||||
{
|
||||
return (T)(object)jsonElement.GetInt32();
|
||||
}
|
||||
if (targetType == typeof(long))
|
||||
{
|
||||
return (T)(object)jsonElement.GetInt64();
|
||||
}
|
||||
if (targetType == typeof(short))
|
||||
{
|
||||
return (T)(object)jsonElement.GetInt16();
|
||||
}
|
||||
if (targetType == typeof(byte))
|
||||
{
|
||||
return (T)(object)jsonElement.GetByte();
|
||||
}
|
||||
if (targetType == typeof(uint))
|
||||
{
|
||||
return (T)(object)jsonElement.GetUInt32();
|
||||
}
|
||||
if (targetType == typeof(ulong))
|
||||
{
|
||||
return (T)(object)jsonElement.GetUInt64();
|
||||
}
|
||||
if (targetType == typeof(ushort))
|
||||
{
|
||||
return (T)(object)jsonElement.GetUInt16();
|
||||
}
|
||||
if (targetType == typeof(float))
|
||||
{
|
||||
return (T)(object)jsonElement.GetSingle();
|
||||
}
|
||||
if (targetType == typeof(double))
|
||||
{
|
||||
return (T)(object)jsonElement.GetDouble();
|
||||
}
|
||||
}
|
||||
|
||||
// 其他类型使用标准反序列化
|
||||
return jsonElement.Deserialize<T>(jsonOptions) ?? defaultValue;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// ⚠️ 如果是直接从 cache 读取的值,也可能需要类型转换
|
||||
// 例如:double -> int
|
||||
if (value is double doubleValue)
|
||||
{
|
||||
Type targetType = typeof(T);
|
||||
if (targetType == typeof(int))
|
||||
{
|
||||
return (T)(object)(int)doubleValue;
|
||||
}
|
||||
if (targetType == typeof(long))
|
||||
{
|
||||
return (T)(object)(long)doubleValue;
|
||||
}
|
||||
if (targetType == typeof(short))
|
||||
{
|
||||
return (T)(object)(short)doubleValue;
|
||||
}
|
||||
if (targetType == typeof(byte))
|
||||
{
|
||||
return (T)(object)(byte)doubleValue;
|
||||
}
|
||||
}
|
||||
|
||||
return (T)value;
|
||||
}
|
||||
|
||||
Set(key, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void Set<T>(string key, T value)
|
||||
{
|
||||
cache[key] = value;
|
||||
SaveToFile();
|
||||
}
|
||||
|
||||
private void LoadFromFile()
|
||||
{
|
||||
lock (fileLock)
|
||||
{
|
||||
if (!File.Exists(settingsFilePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(settingsFilePath);
|
||||
Dictionary<string, JsonElement>? data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json, jsonOptions);
|
||||
if (data is not null)
|
||||
{
|
||||
foreach ((string key, JsonElement value) in data)
|
||||
{
|
||||
cache[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If file is corrupted, start fresh
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveToFile()
|
||||
{
|
||||
lock (fileLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Convert cache to serializable dictionary
|
||||
Dictionary<string, object?> serializableData = new(cache);
|
||||
string json = JsonSerializer.Serialize(serializableData, jsonOptions);
|
||||
File.WriteAllText(settingsFilePath, json);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to save settings: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Converter for ApplicationDataCompositeValue
|
||||
private sealed class ApplicationDataCompositeValueJsonConverter : System.Text.Json.Serialization.JsonConverter<ApplicationDataCompositeValue>
|
||||
{
|
||||
public override ApplicationDataCompositeValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ApplicationDataCompositeValue composite = new();
|
||||
while (reader.Read())
|
||||
{
|
||||
if (reader.TokenType == JsonTokenType.EndObject)
|
||||
{
|
||||
return composite;
|
||||
}
|
||||
|
||||
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string? key = reader.GetString();
|
||||
reader.Read();
|
||||
|
||||
if (key is not null)
|
||||
{
|
||||
composite[key] = reader.TokenType switch
|
||||
{
|
||||
JsonTokenType.String => reader.GetString(),
|
||||
JsonTokenType.Number => reader.TryGetInt64(out long l) ? l : reader.GetDouble(),
|
||||
JsonTokenType.True => true,
|
||||
JsonTokenType.False => false,
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return composite;
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, ApplicationDataCompositeValue value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStartObject();
|
||||
foreach ((string key, object? val) in value)
|
||||
{
|
||||
writer.WritePropertyName(key);
|
||||
if (val is null)
|
||||
{
|
||||
writer.WriteNullValue();
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonSerializer.Serialize(writer, val, val.GetType(), options);
|
||||
}
|
||||
}
|
||||
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,27 @@ internal sealed class ProcessFactory
|
||||
{
|
||||
string repoDirectory = HutaoRuntime.GetDataRepositoryDirectory();
|
||||
string fullTrustFilePath = Path.Combine(repoDirectory, "Snap.ContentDelivery", "Snap.Hutao.FullTrust.exe");
|
||||
|
||||
// Check if FullTrust executable exists - if not, fallback to normal admin mode
|
||||
if (!File.Exists(fullTrustFilePath))
|
||||
{
|
||||
string errorMessage = $"""
|
||||
Island 功能需要的 FullTrust 进程文件不存在,将使用普通管理员模式启动游戏。
|
||||
预期路径:{fullTrustFilePath}
|
||||
|
||||
原因:ContentDelivery 仓库尚未下载或初始化失败(常见于非打包模式首次运行)
|
||||
|
||||
Island 功能将不可用,但游戏可以正常启动。
|
||||
等待仓库下载完成后可重新尝试使用 Island 功能。
|
||||
""";
|
||||
|
||||
// Capture as breadcrumb instead of exception
|
||||
SentrySdk.AddBreadcrumb(errorMessage, category: "process.fulltrust", level: Sentry.BreadcrumbLevel.Warning);
|
||||
|
||||
// Fallback to normal admin mode - Island features will not work but game can launch
|
||||
return CreateUsingShellExecuteRunAs(arguments, fileName, workingDirectory);
|
||||
}
|
||||
|
||||
StartUsingShellExecuteRunAs(fullTrustFilePath);
|
||||
|
||||
FullTrustProcessStartInfoRequest request = new()
|
||||
|
||||
@@ -77,21 +77,47 @@ internal sealed class GameIslandInterop : IGameIslandInterop
|
||||
{
|
||||
nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle();
|
||||
InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea);
|
||||
|
||||
if (!resume)
|
||||
{
|
||||
if (context.Process is not FullTrustProcess fullTrustProcess)
|
||||
{
|
||||
throw HutaoException.InvalidOperation("Process is not full trust");
|
||||
}
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(islandPath);
|
||||
if (!File.Exists(islandPath))
|
||||
{
|
||||
throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists);
|
||||
}
|
||||
|
||||
fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath));
|
||||
fullTrustProcess.ResumeMainThread();
|
||||
// Support both FullTrust and normal admin mode
|
||||
if (context.Process is FullTrustProcess fullTrustProcess)
|
||||
{
|
||||
// Use FullTrust process for injection (suspended process)
|
||||
fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath));
|
||||
fullTrustProcess.ResumeMainThread();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Use native injection for normal admin mode
|
||||
// The process was already started by CreateUsingShellExecuteRunAs
|
||||
// Just inject the DLL into the running process
|
||||
try
|
||||
{
|
||||
// Wait a bit for process to initialize
|
||||
// await Task.Delay(5000, token).ConfigureAwait(false);
|
||||
|
||||
// Inject using RemoteThread
|
||||
DllInjectionUtilities.InjectUsingRemoteThread(islandPath, context.Process.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log the injection failure but don't crash - game can still run
|
||||
SentrySdk.AddBreadcrumb(
|
||||
$"Island DLL injection failed: {ex.Message}",
|
||||
category: "island.injection",
|
||||
level: Sentry.BreadcrumbLevel.Error);
|
||||
|
||||
// Re-throw to let the caller handle it
|
||||
throw HutaoException.Throw($"Island DLL 注入失败: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await PeriodicUpdateIslandEnvironmentAsync(context, handle, token).ConfigureAwait(false);
|
||||
|
||||
@@ -45,6 +45,7 @@ internal sealed class GameProcessFactory
|
||||
string gameFilePath = context.FileSystem.GameFilePath;
|
||||
string gameDirectory = context.FileSystem.GameDirectory;
|
||||
|
||||
// ProcessFactory.CreateUsingFullTrustSuspended will automatically fallback to normal mode if FullTrust.exe is missing
|
||||
return launchOptions.IsIslandEnabled.Value
|
||||
? ProcessFactory.CreateUsingFullTrustSuspended(commandLine, gameFilePath, gameDirectory)
|
||||
: ProcessFactory.CreateUsingShellExecuteRunAs(commandLine, gameFilePath, gameDirectory);
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
// Licensed under the MIT license.
|
||||
|
||||
using Snap.Hutao.Core.IO.Hashing;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Web.Hutao;
|
||||
using System.Collections.Immutable;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using Windows.Storage;
|
||||
|
||||
namespace Snap.Hutao.Service.Git;
|
||||
|
||||
internal static class RepositoryAffinity
|
||||
{
|
||||
private static readonly ApplicationDataContainer RepositoryContainer = ApplicationData.Current.LocalSettings.CreateContainer("RepositoryAffinity", ApplicationDataCreateDisposition.Always);
|
||||
private const string RepositoryAffinityPrefix = "RepositoryAffinity::";
|
||||
private static readonly Lock SyncRoot = new();
|
||||
|
||||
public static ImmutableArray<GitRepository> Sort(ImmutableArray<GitRepository> repositories)
|
||||
@@ -23,9 +23,8 @@ internal static class RepositoryAffinity
|
||||
for (int i = 0; i < repositories.Length; i++)
|
||||
{
|
||||
GitRepository repository = repositories[i];
|
||||
ApplicationDataContainer container = RepositoryContainer.CreateContainer(repository.Name, ApplicationDataCreateDisposition.Always);
|
||||
string key = Hash.ToHexString(HashAlgorithmName.SHA256, repository.HttpsUrl.OriginalString.ToUpperInvariant());
|
||||
counts[i] = container.Values[key] is int c ? c : 0;
|
||||
string key = GetSettingKey(repository.Name, repository.HttpsUrl.OriginalString);
|
||||
counts[i] = LocalSetting.Get(key, 0);
|
||||
}
|
||||
|
||||
Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories));
|
||||
@@ -42,10 +41,9 @@ internal static class RepositoryAffinity
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always);
|
||||
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
|
||||
object box = container.Values[key];
|
||||
container.Values[key] = box is int count ? unchecked(count + 1) : 1;
|
||||
string key = GetSettingKey(name, url);
|
||||
int currentCount = LocalSetting.Get(key, 0);
|
||||
LocalSetting.Set(key, unchecked(currentCount + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +56,15 @@ internal static class RepositoryAffinity
|
||||
{
|
||||
lock (SyncRoot)
|
||||
{
|
||||
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always);
|
||||
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
|
||||
object box = container.Values[key];
|
||||
container.Values[key] = box is int count ? unchecked(count - 1) : 0;
|
||||
string key = GetSettingKey(name, url);
|
||||
int currentCount = LocalSetting.Get(key, 0);
|
||||
LocalSetting.Set(key, unchecked(currentCount - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetSettingKey(string name, string url)
|
||||
{
|
||||
string urlHash = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
|
||||
return $"{RepositoryAffinityPrefix}{name}::{urlHash}";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
||||
@@ -47,12 +47,18 @@
|
||||
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
||||
<PackageCertificateKeyFile>Snap.Hutao_TemporaryKey.pfx</PackageCertificateKeyFile>
|
||||
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
||||
<!-- 关闭AppX打包 -->
|
||||
<AppxPackage>false</AppxPackage>
|
||||
<!-- 不设置打包类型 -->
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Package.appxmanifest" />
|
||||
<None Remove="Package.development.appxmanifest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
<AppxManifest Include="Package.appxmanifest" Condition="'$(ConfigurationName)'!='Debug'" />
|
||||
<AppxManifest Include="Package.development.appxmanifest" Condition="'$(ConfigurationName)'=='Debug'" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="DeleteLibsAfterBuild" AfterTargets="Build">
|
||||
@@ -241,15 +247,15 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251105-build.1544" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251105-build.1544" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251105-build.1544" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
|
||||
<PackageReference Include="Google.Protobuf" Version="3.33.1" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
@@ -265,7 +271,7 @@
|
||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.13.22" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
|
||||
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.3.0-prerelease.251015.2" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||
<PackageReference Include="QRCoder" Version="1.7.0" />
|
||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
||||
@@ -274,10 +280,6 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="2.5.12">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
|
||||
@@ -553,7 +553,7 @@
|
||||
</ScrollViewer>
|
||||
</PivotItem>
|
||||
|
||||
<PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}" IsEnabled="False">
|
||||
<PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}">
|
||||
<Border
|
||||
Margin="16"
|
||||
Padding="0"
|
||||
|
||||
@@ -105,7 +105,7 @@ internal static class StaticResource
|
||||
|
||||
foreach ((string key, object value) in map)
|
||||
{
|
||||
if ((int)value < (int)LatestResourceVersionMap[key])
|
||||
if (Convert.ToInt32(value) < Convert.ToInt32(LatestResourceVersionMap[key]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -120,7 +120,7 @@ internal static class StaticResource
|
||||
ApplicationDataCompositeValue map = LocalSetting.Get(ContractMap, DefaultResourceVersionMap);
|
||||
foreach ((string key, object value) in LatestResourceVersionMap)
|
||||
{
|
||||
if (!map.TryGetValue(key, out object current) || (int)value > (int)current)
|
||||
if (!map.TryGetValue(key, out object current) || Convert.ToInt32(value) > Convert.ToInt32(current))
|
||||
{
|
||||
result.Add(key);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ApplicationModel;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
using Snap.Hutao.Factory.ContentDialog;
|
||||
using Snap.Hutao.Factory.Picker;
|
||||
@@ -71,8 +72,18 @@ internal sealed class SettingStorageSetDataFolderOperation
|
||||
try
|
||||
{
|
||||
Directory.SetReadOnly(oldFolderPath, false);
|
||||
StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath);
|
||||
await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false);
|
||||
|
||||
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||
{
|
||||
// Packaged: use StorageFolder API
|
||||
StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath);
|
||||
await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unpackaged: use standard file I/O
|
||||
await CopyDirectoryAsync(oldFolderPath, newFolderPath).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -84,4 +95,22 @@ internal sealed class SettingStorageSetDataFolderOperation
|
||||
LocalSetting.Set(SettingKeys.DataDirectory, newFolderPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async ValueTask CopyDirectoryAsync(string sourceDir, string destDir)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
// Create all directories
|
||||
foreach (string dirPath in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
Directory.CreateDirectory(dirPath.Replace(sourceDir, destDir));
|
||||
}
|
||||
|
||||
// Copy all files
|
||||
foreach (string filePath in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
File.Copy(filePath, filePath.Replace(sourceDir, destDir), true);
|
||||
}
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.Windows.AppLifecycle;
|
||||
using Snap.Hutao.Core;
|
||||
using Snap.Hutao.Core.ApplicationModel;
|
||||
using Snap.Hutao.Core.Caching;
|
||||
using Snap.Hutao.Core.Logging;
|
||||
using Snap.Hutao.Core.Setting;
|
||||
@@ -135,7 +136,18 @@ internal sealed partial class SettingStorageViewModel : Abstraction.ViewModel
|
||||
// TODO: prompt user that restart will be non-elevated
|
||||
try
|
||||
{
|
||||
AppInstance.Restart(string.Empty);
|
||||
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||
{
|
||||
AppInstance.Restart(string.Empty);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Unpackaged: manually restart the process
|
||||
string exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName
|
||||
?? throw new InvalidOperationException("Cannot get process path");
|
||||
System.Diagnostics.Process.Start(exePath);
|
||||
Snap.Hutao.Factory.Process.ProcessFactory.KillCurrent();
|
||||
}
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
// Copyright (c) DGP Studio. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
|
||||
namespace Snap.Hutao.Web.Hoyolab.DataSigning;
|
||||
|
||||
/// <summary>
|
||||
/// Salt constants for data signing
|
||||
/// Values are obtained from https://github.com/UIGF-org/Hoyolab.Salt
|
||||
/// This file should normally be generated by Snap.Hutao.SourceGeneration.Automation.SaltConstantGenerator
|
||||
/// But is provided manually when the generator fails to fetch values from the network.
|
||||
///
|
||||
/// IMPORTANT: For local builds, you must manually obtain salt values from:
|
||||
/// https://github.com/UIGF-org/Hoyolab.Salt
|
||||
/// </summary>
|
||||
|
||||
internal static class SaltConstants
|
||||
{
|
||||
// Version numbers - Update these according to the current miHoYo app versions
|
||||
public const string CNVersion = "2.95.1";
|
||||
public const string OSVersion = "2.54.0";
|
||||
|
||||
// Salt keys for Chinese (CN) server
|
||||
// These are placeholder values - MUST be replaced with actual values from UIGF-org/Hoyolab.Salt
|
||||
public const string CNK2 = "sfYPEgpxkOe1I3XVMLdwp1Lyt9ORgZsq";
|
||||
public const string CNLK2 = "sidQFEglajEz7FA0Aj7HQPV88zpf17SO";
|
||||
|
||||
// Salt keys for Overseas (OS) server
|
||||
public const string OSK2 = "599uqkwc0dlqu3h6epzjzfhgyyrd44ae";
|
||||
public const string OSLK2 = "rk4xg2hakoi26nljpr099fv9fck1ah10";
|
||||
|
||||
// Note: The actual salt values are security-sensitive and should not be committed
|
||||
// to public repositories. For local builds, obtain them from the UIGF organization
|
||||
// and replace the placeholders above.
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user