支持非打包模式直接启动并更改优化设置存储逻辑采用wix 打包 msi 安装包支持脱离沙箱使用,为注入功能开发奠定基础

新增 PackageIdentityAdapter 适配打包和非打包模式,支持通过文件系统路径替代 Windows.Storage.ApplicationData。重构 LocalSetting,抽象为 ISettingStorage 接口,支持 JSON 文件存储非打包模式下的设置。改进异常处理和日志记录,增加调试信息和启动诊断日志。隐藏 AppX 打包配置,更新依赖项版本,支持普通管理员模式下的游戏启动和 DLL 注入。补全 SaltConstants 文件,存储数据签名所需的盐值。

已知问题:
注入功能暂不不支持,缺少服务器提供的当前版本注入信息!
没有启用默认管理员权限打开
This commit is contained in:
hoshiizumiya
2025-11-23 14:34:45 +08:00
parent 7ee92db156
commit e55d6ea1c5
29 changed files with 1037 additions and 129 deletions

2
.gitignore vendored
View File

@@ -25,3 +25,5 @@ src/Snap.Hutao/Snap.Hutao/Generated Files/
tools/ tools/
src/Snap.Hutao/Snap.Hutao/AppPackages src/Snap.Hutao/Snap.Hutao/AppPackages
/src/Snap.Hutao/Snap.Hutao.Installer/obj
/src/Snap.Hutao/Snap.Hutao.Installer/bin/x64/Release/en-US

View File

@@ -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>

View File

@@ -0,0 +1,3 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
</Wix>

View 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>

View 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>

View File

@@ -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>

View File

@@ -1,24 +1,24 @@
<Solution> <Solution>
<Configurations> <Configurations>
<Platform Name="Any CPU" /> <Platform Name="Any CPU" />
<Platform Name="ARM64" />
<Platform Name="x64" /> <Platform Name="x64" />
<Platform Name="x86" />
</Configurations> </Configurations>
<Folder Name="/Solution Items/"> <Folder Name="/Solution Items/">
<File Path=".editorconfig" /> <File Path=".editorconfig" />
<File Path=".vsconfig" /> <File Path=".vsconfig" />
</Folder> </Folder>
<Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj" /> <Project Path="Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj" Type="b7dd6f7e-def8-4e67-b5b7-07ef123db6f0" Id="91a04cd0-28cc-4562-92e1-202bc163edd7">
<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 -->
<Platform Solution="*|Any CPU" Project="x64" /> <Platform Solution="*|Any CPU" Project="x64" />
<Platform Solution="*|arm64" Project="arm64" /> </Project>
<Platform Solution="*|x64" Project="x64" /> <Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj">
<Platform Solution="*|x86" Project="x86" /> <Build Solution="*|ARM64" Project="false" />
<Deploy /> <Build Solution="*|x86" Project="false" />
</Project>
<Project Path="Snap.Hutao\Snap.Hutao.csproj">
<Platform Project="x64" />
<Deploy Solution="*|Any CPU" />
<Deploy Solution="*|x64" />
</Project> </Project>
</Solution> </Solution>

View File

@@ -13,6 +13,7 @@ using Snap.Hutao.Service;
using Snap.Hutao.UI.Xaml; using Snap.Hutao.UI.Xaml;
using Snap.Hutao.UI.Xaml.Control.Theme; using Snap.Hutao.UI.Xaml.Control.Theme;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
namespace Snap.Hutao; namespace Snap.Hutao;
@@ -64,6 +65,11 @@ public sealed partial class App : Application
protected override void OnLaunched(LaunchActivatedEventArgs args) protected override void OnLaunched(LaunchActivatedEventArgs args)
{ {
// ⚠️ 添加启动诊断
#if DEBUG
Core.ApplicationModel.PackageIdentityDiagnostics.LogDiagnostics();
#endif
DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT(); DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT();
try try
@@ -71,18 +77,41 @@ public sealed partial class App : Application
// Important: You must call AppNotificationManager::Default().Register // Important: You must call AppNotificationManager::Default().Register
// before calling AppInstance.GetCurrent.GetActivatedEventArgs. // before calling AppInstance.GetCurrent.GetActivatedEventArgs.
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked; AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
try
{
AppNotificationManager.Default.Register(); AppNotificationManager.Default.Register();
}
catch
{
// In unpackaged mode, this might fail - continue anyway
}
// E_INVALIDARG E_OUTOFMEMORY // E_INVALIDARG E_OUTOFMEMORY
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs(); AppActivationArguments? activatedEventArgs = null;
PrivateNamedPipeClient? namedPipeClient = null;
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs)) try
{
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")); SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao"));
XamlApplicationLifetime.ActivationAndInitializationCompleted = true; XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
Exit(); Exit();
return; return;
} }
}
logger.LogInformation($"{ConsoleBanner}"); logger.LogInformation($"{ConsoleBanner}");
@@ -90,10 +119,30 @@ public sealed partial class App : Application
// Manually invoke // Manually invoke
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Activate and Initialize", "Application")); 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) 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.CaptureException(ex);
SentrySdk.Flush(); SentrySdk.Flush();

View File

@@ -31,8 +31,15 @@ public static partial class Bootstrap
[STAThread] [STAThread]
private static void Main(string[] args) private static void Main(string[] args)
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Starting...");
#endif
if (Mutex.TryOpenExisting(LockName, out _)) if (Mutex.TryOpenExisting(LockName, out _))
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Another instance is running");
#endif
return; return;
} }
@@ -42,9 +49,16 @@ public static partial class Bootstrap
mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow)); mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow));
mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity); mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity);
Debug.Assert(created); Debug.Assert(created);
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Mutex created");
#endif
} }
catch (WaitHandleCannotBeOpenedException) catch (WaitHandleCannotBeOpenedException)
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] WaitHandleCannotBeOpenedException");
#endif
return; return;
} }
@@ -54,34 +68,70 @@ public static partial class Bootstrap
{ {
if (!OSPlatformSupported()) if (!OSPlatformSupported())
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] OS not supported");
#endif
return; return;
} }
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Setting environment variables");
#endif
Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000"); Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000");
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128"); Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128");
AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false); AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false);
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Initializing COM wrappers");
#endif
ComWrappersSupport.InitializeComWrappers(); 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 // By adding the using statement, we can dispose the injected services when closing
using (ServiceProvider serviceProvider = DependencyInjection.Initialize()) using (ServiceProvider serviceProvider = DependencyInjection.Initialize())
{ {
Thread.CurrentThread.Name = "Snap Hutao Application Main Thread"; 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 // If you hit a COMException REGDB_E_CLASSNOTREG (0x80040154) during debugging
// You can delete bin and obj folder and then rebuild. // You can delete bin and obj folder and then rebuild.
// In a Desktop app this runs a message pump internally, // In a Desktop app this runs a message pump internally,
// and does not return until the application shuts down. // and does not return until the application shuts down.
Application.Start(AppInitializationCallback); Application.Start(AppInitializationCallback);
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Application.Start() returned");
#endif
XamlApplicationLifetime.Exited = true; XamlApplicationLifetime.Exited = true;
} }
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Flushing Sentry");
#endif
SentrySdk.Flush(); SentrySdk.Flush();
} }
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Exiting");
#endif
} }
private static void InitializeApp(ApplicationInitializationCallbackParams param) private static void InitializeApp(ApplicationInitializationCallbackParams param)
{ {
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Callback invoked");
#endif
Gen2GcCallback.Register(() => Gen2GcCallback.Register(() =>
{ {
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime")); SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime"));
@@ -90,8 +140,17 @@ public static partial class Bootstrap
IServiceProvider serviceProvider = Ioc.Default; IServiceProvider serviceProvider = Ioc.Default;
_ = serviceProvider.GetRequiredService<ITaskContext>(); #if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Creating App instance");
#endif
// ⚠️ 只创建 App
// TaskContext 将在第一次被需要时自动创建(延迟初始化)
_ = serviceProvider.GetRequiredService<App>(); _ = serviceProvider.GetRequiredService<App>();
#if DEBUG
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Initialization complete (TaskContext will be lazily created)");
#endif
} }
private static bool OSPlatformSupported() private static bool OSPlatformSupported()

View File

@@ -4,14 +4,13 @@
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Windows.ApplicationModel;
namespace Snap.Hutao.Core.ApplicationModel; namespace Snap.Hutao.Core.ApplicationModel;
internal static class LimitedAccessFeatures internal static class LimitedAccessFeatures
{ {
private static readonly string PackagePublisherId = Package.Current.Id.PublisherId; private static readonly string PackagePublisherId = PackageIdentityAdapter.PublisherId;
private static readonly string PackageFamilyName = Package.Current.Id.FamilyName; private static readonly string PackageFamilyName = PackageIdentityAdapter.FamilyName;
private static readonly FrozenDictionary<string, string> Features = WinRTAdaptive.ToFrozenDictionary( 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") 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)); return Windows.ApplicationModel.LimitedAccessFeatures.TryUnlockFeature(featureId, GetToken(featureId), GetAttestation(featureId));
} }

View File

@@ -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";
}
}

View File

@@ -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}");
}
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Model.Entity.Database; using Snap.Hutao.Model.Entity.Database;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
@@ -17,7 +18,20 @@ internal sealed partial class HutaoDiagnostics : IHutaoDiagnostics
[GeneratedConstructor] [GeneratedConstructor]
public partial HutaoDiagnostics(IServiceProvider serviceProvider); 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) public async ValueTask<int> ExecuteSqlAsync(string sql)
{ {

View File

@@ -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")] [SuppressMessage("", "SH001", Justification = "IHutaoDiagnostics must be public in order to be exposed to the scripting environment")]
public interface IHutaoDiagnostics public interface IHutaoDiagnostics
{ {
ApplicationDataContainer LocalSettings { get; } ApplicationDataContainer? LocalSettings { get; }
ValueTask<int> ExecuteSqlAsync(string sql); ValueTask<int> ExecuteSqlAsync(string sql);

View File

@@ -4,6 +4,7 @@
using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Core;
using Microsoft.Win32; using Microsoft.Win32;
using Microsoft.Windows.AppNotifications; using Microsoft.Windows.AppNotifications;
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO; using Snap.Hutao.Core.IO;
using Snap.Hutao.Core.IO.Hashing; using Snap.Hutao.Core.IO.Hashing;
@@ -13,31 +14,32 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Windows.ApplicationModel;
using Windows.Storage;
namespace Snap.Hutao.Core; namespace Snap.Hutao.Core;
internal static class HutaoRuntime 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 UserAgent { get; } = $"Snap Hutao/{Version}";
public static string DataDirectory { get; } = InitializeDataDirectory(); 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 string DeviceId { get; } = InitializeDeviceId();
public static WebView2Version WebView2Version { get; } = InitializeWebView2(); 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 // Requires main thread
public static bool IsAppNotificationEnabled { get; } = AppNotificationManager.Default.Setting is AppNotificationSetting.Enabled; public static bool IsAppNotificationEnabled { get; } = CheckAppNotificationEnabled();
public static string? GetDisplayName() public static string? GetDisplayName()
{ {
@@ -106,32 +108,57 @@ internal static class HutaoRuntime
return string.Intern(directory); return string.Intern(directory);
} }
private static string InitializeDataDirectory() private static bool GetIsProcessElevated()
{ {
// Delete the previous data folder if it exists // ⚠️ 这里调用 LocalSetting 时,确保 DataDirectory 已经初始化完成
try try
{ {
string previousDirectory = LocalSetting.Get(SettingKeys.PreviousDataDirectoryToDelete, string.Empty); return LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false) || Environment.IsPrivilegedProcess;
if (!string.IsNullOrEmpty(previousDirectory) && Directory.Exists(previousDirectory))
{
Directory.SetReadOnly(previousDirectory, false);
Directory.Delete(previousDirectory, true);
} }
} catch
finally
{ {
LocalSetting.Set(SettingKeys.PreviousDataDirectoryToDelete, string.Empty); // 如果读取失败,使用默认值
return Environment.IsPrivilegedProcess;
}
} }
// Check if the preferred path is set private static string InitializeLocalCacheDirectory()
string currentDirectory = LocalSetting.Get(SettingKeys.DataDirectory, string.Empty);
if (!string.IsNullOrEmpty(currentDirectory))
{ {
Directory.CreateDirectory(currentDirectory); if (PackageIdentityAdapter.HasPackageIdentity)
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 const string FolderName
#if IS_ALPHA_BUILD #if IS_ALPHA_BUILD
= "HutaoAlpha"; = "HutaoAlpha";
@@ -141,30 +168,43 @@ internal static class HutaoRuntime
= "Hutao"; = "Hutao";
#endif #endif
// ⚠️ 不要在这里调用 LocalSetting - 会导致循环依赖
// 先确定默认的数据目录位置
// Check if the old documents path exists // Check if the old documents path exists
string myDocumentsHutaoDirectory = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FolderName)); string myDocumentsHutaoDirectory = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FolderName));
if (Directory.Exists(myDocumentsHutaoDirectory)) if (Directory.Exists(myDocumentsHutaoDirectory))
{ {
LocalSetting.Set(SettingKeys.DataDirectory, myDocumentsHutaoDirectory);
return myDocumentsHutaoDirectory; return myDocumentsHutaoDirectory;
} }
// Prefer LocalApplicationData // Use LocalApplicationData
string localApplicationData = ApplicationData.Current.LocalFolder.Path; string localApplicationData;
string path = Path.GetFullPath(Path.Combine(localApplicationData, FolderName)); 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 try
{ {
Directory.CreateDirectory(path); Directory.CreateDirectory(defaultPath);
} }
catch (Exception ex) catch (Exception ex)
{ {
// FileNotFoundException | UnauthorizedAccessException // FileNotFoundException | UnauthorizedAccessException
// We don't have enough permission HutaoException.InvalidOperation($"Failed to create data folder: {defaultPath}", ex);
HutaoException.InvalidOperation($"Failed to create data folder: {path}", ex);
} }
LocalSetting.Set(SettingKeys.DataDirectory, path); return defaultPath;
return path;
} }
private static string InitializeDeviceId() private static string InitializeDeviceId()

View File

@@ -1,11 +1,10 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.ApplicationModel;
using System.IO; using System.IO;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal; using System.Security.Principal;
using Windows.ApplicationModel;
using Windows.Storage;
namespace Snap.Hutao.Core; namespace Snap.Hutao.Core;
@@ -13,7 +12,7 @@ internal static class InstalledLocation
{ {
public static string GetAbsolutePath(string relativePath) 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) public static void CopyFileFromApplicationUri(string url, string path)
@@ -23,8 +22,26 @@ internal static class InstalledLocation
static async Task CopyApplicationUriFileCoreAsync(string url, string path) static async Task CopyApplicationUriFileCoreAsync(string url, string path)
{ {
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding); 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)) if (File.Exists(path))
{ {

View File

@@ -74,8 +74,15 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
public void ActivateAndInitialize(HutaoActivationArguments args) public void ActivateAndInitialize(HutaoActivationArguments args)
{ {
#if DEBUG
Debug.WriteLine("[AppActivation] ActivateAndInitialize called");
#endif
if (Volatile.Read(ref isActivating) is 1) if (Volatile.Read(ref isActivating) is 1)
{ {
#if DEBUG
Debug.WriteLine("[AppActivation] Already activating, returning");
#endif
return; return;
} }
@@ -85,21 +92,52 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
{ {
try try
{ {
#if DEBUG
Debug.WriteLine("[AppActivation] Starting activation process");
#endif
using (await activateLock.LockAsync().ConfigureAwait(false)) using (await activateLock.LockAsync().ConfigureAwait(false))
{ {
if (Interlocked.CompareExchange(ref isActivating, 1, 0) is not 0) if (Interlocked.CompareExchange(ref isActivating, 1, 0) is not 0)
{ {
#if DEBUG
Debug.WriteLine("[AppActivation] Race condition detected, returning");
#endif
return; return;
} }
#if DEBUG
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleActivationAsync");
#endif
await UnsynchronizedHandleActivationAsync(args).ConfigureAwait(false); await UnsynchronizedHandleActivationAsync(args).ConfigureAwait(false);
#if DEBUG
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleInitializationAsync");
#endif
await UnsynchronizedHandleInitializationAsync().ConfigureAwait(false); 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 finally
{ {
XamlApplicationLifetime.ActivationAndInitializationCompleted = true; XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
Interlocked.Exchange(ref isActivating, 0); 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>() private async ValueTask<Window?> WaitWindowAsync<TWindow>()
where TWindow : Window where TWindow : Window
{ {
#if DEBUG
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Waiting for window type: {typeof(TWindow).Name}");
#endif
await taskContext.SwitchToMainThreadAsync(); await taskContext.SwitchToMainThreadAsync();
#if DEBUG
Debug.WriteLine("[AppActivation.WaitWindowAsync] Switched to main thread");
#endif
if (currentXamlWindowReference.Window is not { } window) if (currentXamlWindowReference.Window is not { } window)
{ {
#if DEBUG
Debug.WriteLine("[AppActivation.WaitWindowAsync] Creating new window instance");
#endif
try try
{ {
window = serviceProvider.GetRequiredService<TWindow>(); 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) if (XamlApplicationLifetime.Exiting)
{ {
return default; return default;
@@ -330,11 +388,33 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
throw; throw;
} }
catch (Exception ex)
{
#if DEBUG
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Exception creating window: {ex}");
#endif
throw;
}
currentXamlWindowReference.Window = window; 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(); window.SwitchTo();
#if DEBUG
Debug.WriteLine("[AppActivation.WaitWindowAsync] Window activated");
#endif
window.AppWindow?.MoveInZOrderAtTop(); window.AppWindow?.MoveInZOrderAtTop();
return window; return window;
} }

View File

@@ -66,4 +66,14 @@ internal sealed class HutaoActivationArguments
return result; return result;
} }
public static HutaoActivationArguments CreateDefaultLaunchArguments()
{
return new HutaoActivationArguments
{
IsRedirectTo = false,
Kind = HutaoActivationKind.Launch,
LaunchActivatedArguments = string.Empty
};
}
} }

View File

@@ -1,12 +1,16 @@
// Copyright (c) DGP Studio. All rights reserved. // Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Core.ExceptionService; using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Factory.Process; using Snap.Hutao.Factory.Process;
using Snap.Hutao.Win32; using Snap.Hutao.Win32;
using Snap.Hutao.Win32.Foundation; using Snap.Hutao.Win32.Foundation;
using System.Collections.Concurrent;
using System.Collections.Frozen; using System.Collections.Frozen;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Text.Json;
using Windows.Storage; using Windows.Storage;
namespace Snap.Hutao.Core.Setting; namespace Snap.Hutao.Core.Setting;
@@ -36,40 +40,20 @@ internal static class LocalSetting
typeof(ApplicationDataCompositeValue) 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!) public static T Get<T>(string key, T defaultValue = default!)
{ {
Debug.Assert(SupportedTypes.Contains(typeof(T))); Debug.Assert(SupportedTypes.Contains(typeof(T)));
if (Container.Values.TryGetValue(key, out object? value)) return Storage.Get(key, defaultValue);
{
// unbox the value
return value is null ? defaultValue : (T)value;
}
Set(key, defaultValue);
return defaultValue;
} }
public static void Set<T>(string key, T value) public static void Set<T>(string key, T value)
{ {
Debug.Assert(SupportedTypes.Contains(typeof(T))); Debug.Assert(SupportedTypes.Contains(typeof(T)));
Storage.Set(key, 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;
}
} }
public static void SetIf<T>(bool condition, string key, T value) public static void SetIf<T>(bool condition, string key, T value)
@@ -103,4 +87,299 @@ internal static class LocalSetting
Set(key, newValue); Set(key, newValue);
return oldValue; 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();
}
}
} }

View File

@@ -147,6 +147,27 @@ internal sealed class ProcessFactory
{ {
string repoDirectory = HutaoRuntime.GetDataRepositoryDirectory(); string repoDirectory = HutaoRuntime.GetDataRepositoryDirectory();
string fullTrustFilePath = Path.Combine(repoDirectory, "Snap.ContentDelivery", "Snap.Hutao.FullTrust.exe"); 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); StartUsingShellExecuteRunAs(fullTrustFilePath);
FullTrustProcessStartInfoRequest request = new() FullTrustProcessStartInfoRequest request = new()

View File

@@ -77,22 +77,48 @@ internal sealed class GameIslandInterop : IGameIslandInterop
{ {
nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle(); nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle();
InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea); InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea);
if (!resume) if (!resume)
{ {
if (context.Process is not FullTrustProcess fullTrustProcess)
{
throw HutaoException.InvalidOperation("Process is not full trust");
}
ArgumentException.ThrowIfNullOrEmpty(islandPath); ArgumentException.ThrowIfNullOrEmpty(islandPath);
if (!File.Exists(islandPath)) if (!File.Exists(islandPath))
{ {
throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists); throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists);
} }
// 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.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath));
fullTrustProcess.ResumeMainThread(); 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); await PeriodicUpdateIslandEnvironmentAsync(context, handle, token).ConfigureAwait(false);
} }

View File

@@ -45,6 +45,7 @@ internal sealed class GameProcessFactory
string gameFilePath = context.FileSystem.GameFilePath; string gameFilePath = context.FileSystem.GameFilePath;
string gameDirectory = context.FileSystem.GameDirectory; string gameDirectory = context.FileSystem.GameDirectory;
// ProcessFactory.CreateUsingFullTrustSuspended will automatically fallback to normal mode if FullTrust.exe is missing
return launchOptions.IsIslandEnabled.Value return launchOptions.IsIslandEnabled.Value
? ProcessFactory.CreateUsingFullTrustSuspended(commandLine, gameFilePath, gameDirectory) ? ProcessFactory.CreateUsingFullTrustSuspended(commandLine, gameFilePath, gameDirectory)
: ProcessFactory.CreateUsingShellExecuteRunAs(commandLine, gameFilePath, gameDirectory); : ProcessFactory.CreateUsingShellExecuteRunAs(commandLine, gameFilePath, gameDirectory);

View File

@@ -2,17 +2,17 @@
// Licensed under the MIT license. // Licensed under the MIT license.
using Snap.Hutao.Core.IO.Hashing; using Snap.Hutao.Core.IO.Hashing;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Web.Hutao; using Snap.Hutao.Web.Hutao;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using Windows.Storage;
namespace Snap.Hutao.Service.Git; namespace Snap.Hutao.Service.Git;
internal static class RepositoryAffinity 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(); private static readonly Lock SyncRoot = new();
public static ImmutableArray<GitRepository> Sort(ImmutableArray<GitRepository> repositories) public static ImmutableArray<GitRepository> Sort(ImmutableArray<GitRepository> repositories)
@@ -23,9 +23,8 @@ internal static class RepositoryAffinity
for (int i = 0; i < repositories.Length; i++) for (int i = 0; i < repositories.Length; i++)
{ {
GitRepository repository = repositories[i]; GitRepository repository = repositories[i];
ApplicationDataContainer container = RepositoryContainer.CreateContainer(repository.Name, ApplicationDataCreateDisposition.Always); string key = GetSettingKey(repository.Name, repository.HttpsUrl.OriginalString);
string key = Hash.ToHexString(HashAlgorithmName.SHA256, repository.HttpsUrl.OriginalString.ToUpperInvariant()); counts[i] = LocalSetting.Get(key, 0);
counts[i] = container.Values[key] is int c ? c : 0;
} }
Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories)); Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories));
@@ -42,10 +41,9 @@ internal static class RepositoryAffinity
{ {
lock (SyncRoot) lock (SyncRoot)
{ {
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always); string key = GetSettingKey(name, url);
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant()); int currentCount = LocalSetting.Get(key, 0);
object box = container.Values[key]; LocalSetting.Set(key, unchecked(currentCount + 1));
container.Values[key] = box is int count ? unchecked(count + 1) : 1;
} }
} }
@@ -58,10 +56,15 @@ internal static class RepositoryAffinity
{ {
lock (SyncRoot) lock (SyncRoot)
{ {
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always); string key = GetSettingKey(name, url);
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant()); int currentCount = LocalSetting.Get(key, 0);
object box = container.Values[key]; LocalSetting.Set(key, unchecked(currentCount - 1));
container.Values[key] = box is int count ? unchecked(count - 1) : 0;
} }
} }
private static string GetSettingKey(string name, string url)
{
string urlHash = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
return $"{RepositoryAffinityPrefix}{name}::{urlHash}";
}
} }

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework> <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
@@ -47,12 +47,18 @@
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled> <AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
<PackageCertificateKeyFile>Snap.Hutao_TemporaryKey.pfx</PackageCertificateKeyFile> <PackageCertificateKeyFile>Snap.Hutao_TemporaryKey.pfx</PackageCertificateKeyFile>
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm> <AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
<!-- 关闭AppX打包 -->
<AppxPackage>false</AppxPackage>
<!-- 不设置打包类型 -->
<WindowsPackageType>None</WindowsPackageType>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<None Remove="Package.appxmanifest" />
<None Remove="Package.development.appxmanifest" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Manifest Include="$(ApplicationManifest)" /> <Manifest Include="$(ApplicationManifest)" />
<AppxManifest Include="Package.appxmanifest" Condition="'$(ConfigurationName)'!='Debug'" />
<AppxManifest Include="Package.development.appxmanifest" Condition="'$(ConfigurationName)'=='Debug'" />
</ItemGroup> </ItemGroup>
<Target Name="DeleteLibsAfterBuild" AfterTargets="Build"> <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.Controls.SettingsControls" Version="8.2.251105-build.1544" />
<PackageReference Include="CommunityToolkit.WinUI.Converters" 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="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="JetBrains.Annotations" Version="2025.2.2" />
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0"> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" /> <PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" /> <PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -265,7 +271,7 @@
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.13.22" /> <PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.13.22" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" /> <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.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="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
<PackageReference Include="QRCoder" Version="1.7.0" /> <PackageReference Include="QRCoder" Version="1.7.0" />
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" /> <PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
@@ -274,10 +280,6 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Snap.Hutao.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"> <PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.3.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>

View File

@@ -553,7 +553,7 @@
</ScrollViewer> </ScrollViewer>
</PivotItem> </PivotItem>
<PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}" IsEnabled="False"> <PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}">
<Border <Border
Margin="16" Margin="16"
Padding="0" Padding="0"

View File

@@ -105,7 +105,7 @@ internal static class StaticResource
foreach ((string key, object value) in map) foreach ((string key, object value) in map)
{ {
if ((int)value < (int)LatestResourceVersionMap[key]) if (Convert.ToInt32(value) < Convert.ToInt32(LatestResourceVersionMap[key]))
{ {
return true; return true;
} }
@@ -120,7 +120,7 @@ internal static class StaticResource
ApplicationDataCompositeValue map = LocalSetting.Get(ContractMap, DefaultResourceVersionMap); ApplicationDataCompositeValue map = LocalSetting.Get(ContractMap, DefaultResourceVersionMap);
foreach ((string key, object value) in LatestResourceVersionMap) 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); result.Add(key);
} }

View File

@@ -3,6 +3,7 @@
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Setting;
using Snap.Hutao.Factory.ContentDialog; using Snap.Hutao.Factory.ContentDialog;
using Snap.Hutao.Factory.Picker; using Snap.Hutao.Factory.Picker;
@@ -71,9 +72,19 @@ internal sealed class SettingStorageSetDataFolderOperation
try try
{ {
Directory.SetReadOnly(oldFolderPath, false); Directory.SetReadOnly(oldFolderPath, false);
if (PackageIdentityAdapter.HasPackageIdentity)
{
// Packaged: use StorageFolder API
StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath); StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath);
await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false); await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false);
} }
else
{
// Unpackaged: use standard file I/O
await CopyDirectoryAsync(oldFolderPath, newFolderPath).ConfigureAwait(false);
}
}
catch (Exception ex) catch (Exception ex)
{ {
Messenger.Send(InfoBarMessage.Error(ex)); Messenger.Send(InfoBarMessage.Error(ex));
@@ -84,4 +95,22 @@ internal sealed class SettingStorageSetDataFolderOperation
LocalSetting.Set(SettingKeys.DataDirectory, newFolderPath); LocalSetting.Set(SettingKeys.DataDirectory, newFolderPath);
return true; 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);
}
} }

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Windows.AppLifecycle; using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core; using Snap.Hutao.Core;
using Snap.Hutao.Core.ApplicationModel;
using Snap.Hutao.Core.Caching; using Snap.Hutao.Core.Caching;
using Snap.Hutao.Core.Logging; using Snap.Hutao.Core.Logging;
using Snap.Hutao.Core.Setting; using Snap.Hutao.Core.Setting;
@@ -134,9 +135,20 @@ internal sealed partial class SettingStorageViewModel : Abstraction.ViewModel
// TODO: prompt user that restart will be non-elevated // TODO: prompt user that restart will be non-elevated
try try
{
if (PackageIdentityAdapter.HasPackageIdentity)
{ {
AppInstance.Restart(string.Empty); 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) catch (COMException ex)
{ {
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_PACKAGE_UPDATING)) if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_PACKAGE_UPDATING))

View File

@@ -1,11 +1,34 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Web.Hoyolab.DataSigning; 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 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 CNVersion = "2.95.1";
public const string OSVersion = "2.54.0"; 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 CNK2 = "sfYPEgpxkOe1I3XVMLdwp1Lyt9ORgZsq";
public const string CNLK2 = "sidQFEglajEz7FA0Aj7HQPV88zpf17SO"; public const string CNLK2 = "sidQFEglajEz7FA0Aj7HQPV88zpf17SO";
// Salt keys for Overseas (OS) server
public const string OSK2 = "599uqkwc0dlqu3h6epzjzfhgyyrd44ae"; public const string OSK2 = "599uqkwc0dlqu3h6epzjzfhgyyrd44ae";
public const string OSLK2 = "rk4xg2hakoi26nljpr099fv9fck1ah10"; 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.
} }