diff --git a/.gitignore b/.gitignore
index 01cf204..103ba00 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,5 +25,6 @@ src/Snap.Hutao/Snap.Hutao/Generated Files/
tools/
src/Snap.Hutao/Snap.Hutao/AppPackages
-/src/Snap.Hutao/Snap.Hutao.Installer/bin
/src/Snap.Hutao/Snap.Hutao.Installer/obj
+/src/Snap.Hutao/Snap.Hutao.Installer/bin/x64/Release/en-US
+/src/Snap.Hutao/Snap.Hutao.Installer/bin
diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs b/src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs
new file mode 100644
index 0000000..7bf4fb7
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl b/src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl
new file mode 100644
index 0000000..7fa02fa
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs b/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs
new file mode 100644
index 0000000..6567b23
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs
@@ -0,0 +1,79 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj b/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj
new file mode 100644
index 0000000..904d477
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj
@@ -0,0 +1,23 @@
+
+
+ ICE03;ICE60
+ x64
+ net10.0-windows10.0.26100.0
+
+
+
+
+
+
+
+
+ MainAppComponents
+ INSTALLFOLDER
+ true
+ true
+ true
+
+
+
+
+
diff --git a/src/Snap.Hutao/Snap.Hutao.slnx b/src/Snap.Hutao/Snap.Hutao.slnx
index b81a126..44e139b 100644
--- a/src/Snap.Hutao/Snap.Hutao.slnx
+++ b/src/Snap.Hutao/Snap.Hutao.slnx
@@ -1,24 +1,24 @@
+
+
-
-
-
-
-
-
-
-
+
-
-
-
-
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs
index d05615d..45548a6 100644
--- a/src/Snap.Hutao/Snap.Hutao/App.xaml.cs
+++ b/src/Snap.Hutao/Snap.Hutao/App.xaml.cs
@@ -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().TryRedirectActivationTo(activatedEventArgs))
+ try
{
- SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao"));
- XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
- Exit();
- return;
+ activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
+ namedPipeClient = serviceProvider.GetRequiredService();
+ }
+ 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();
diff --git a/src/Snap.Hutao/Snap.Hutao/Bootstrap.cs b/src/Snap.Hutao/Snap.Hutao/Bootstrap.cs
index d95e086..0428801 100644
--- a/src/Snap.Hutao/Snap.Hutao/Bootstrap.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Bootstrap.cs
@@ -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();
+ #if DEBUG
+ System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Creating App instance");
+ #endif
+
+ // ⚠️ 只创建 App
+ // TaskContext 将在第一次被需要时自动创建(延迟初始化)
_ = serviceProvider.GetRequiredService();
+
+ #if DEBUG
+ System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Initialization complete (TaskContext will be lazily created)");
+ #endif
}
private static bool OSPlatformSupported()
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/LimitedAccessFeatures.cs b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/LimitedAccessFeatures.cs
index 113993c..0039001 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/LimitedAccessFeatures.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/LimitedAccessFeatures.cs
@@ -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 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));
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityAdapter.cs b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityAdapter.cs
new file mode 100644
index 0000000..7ccd7cb
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityAdapter.cs
@@ -0,0 +1,109 @@
+// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Diagnostics;
+using System.IO;
+using System.Reflection;
+
+namespace Snap.Hutao.Core.ApplicationModel;
+
+///
+/// Adapter to handle both packaged and unpackaged app scenarios
+///
+internal static class PackageIdentityAdapter
+{
+ private static readonly Lazy LazyHasPackageIdentity = new(CheckPackageIdentity);
+ private static readonly Lazy LazyAppDirectory = new(GetAppDirectoryPath);
+ private static readonly Lazy LazyAppVersion = new(GetAppVersionInternal);
+ private static readonly Lazy LazyFamilyName = new(GetFamilyNameInternal);
+ private static readonly Lazy LazyPublisherId = new(GetPublisherIdInternal);
+
+ ///
+ /// Check if the app has package identity
+ ///
+ public static bool HasPackageIdentity => LazyHasPackageIdentity.Value;
+
+ ///
+ /// Get application installation directory
+ ///
+ public static string AppDirectory => LazyAppDirectory.Value;
+
+ ///
+ /// Get application version
+ ///
+ public static Version AppVersion => LazyAppVersion.Value;
+
+ ///
+ /// Get package family name (or fallback for unpackaged)
+ ///
+ public static string FamilyName => LazyFamilyName.Value;
+
+ ///
+ /// Get publisher ID (or fallback for unpackaged)
+ ///
+ 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=Millennium-Science-Technology-R-D-Inst";
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityDiagnostics.cs b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityDiagnostics.cs
new file mode 100644
index 0000000..2397128
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityDiagnostics.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
+// Licensed under the MIT license.
+
+using System.Diagnostics;
+using System.IO;
+
+namespace Snap.Hutao.Core.ApplicationModel;
+
+///
+/// Diagnostic helper for PackageIdentityAdapter
+///
+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}");
+ }
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/HutaoDiagnostics.cs b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/HutaoDiagnostics.cs
index 82e91c3..e5a2de5 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/HutaoDiagnostics.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/HutaoDiagnostics.cs
@@ -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 ExecuteSqlAsync(string sql)
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/IHutaoDiagnostics.cs b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/IHutaoDiagnostics.cs
index a54c436..ca92466 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/IHutaoDiagnostics.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Diagnostics/IHutaoDiagnostics.cs
@@ -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 ExecuteSqlAsync(string sql);
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/HutaoRuntime.cs b/src/Snap.Hutao/Snap.Hutao/Core/HutaoRuntime.cs
index 646e184..44943b1 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/HutaoRuntime.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/HutaoRuntime.cs
@@ -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 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()
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/InstalledLocation.cs b/src/Snap.Hutao/Snap.Hutao/Core/InstalledLocation.cs
index 3eb7a76..4333609 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/InstalledLocation.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/InstalledLocation.cs
@@ -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))
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs
index b9099d5..df3efdc 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/AppActivation.cs
@@ -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 WaitWindowAsync()
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();
+
+ #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;
}
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationArguments.cs b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationArguments.cs
index 10e58d0..e179157 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationArguments.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/LifeCycle/HutaoActivationArguments.cs
@@ -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
+ };
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Core/Setting/LocalSetting.cs b/src/Snap.Hutao/Snap.Hutao/Core/Setting/LocalSetting.cs
index 5fb6a49..2b0a32d 100644
--- a/src/Snap.Hutao/Snap.Hutao/Core/Setting/LocalSetting.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Core/Setting/LocalSetting.cs
@@ -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 LazyStorage = new(CreateStorage);
+
+ private static ISettingStorage Storage => LazyStorage.Value;
public static T Get(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(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(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(string key, T defaultValue);
+ void Set(string key, T value);
+ }
+
+ private sealed class PackagedSettingStorage : ISettingStorage
+ {
+ private readonly ApplicationDataContainer container = ApplicationData.Current.LocalSettings;
+
+ public T Get(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(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 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(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(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(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? data = JsonSerializer.Deserialize>(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 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
+ {
+ 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();
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/Factory/Process/ProcessFactory.cs b/src/Snap.Hutao/Snap.Hutao/Factory/Process/ProcessFactory.cs
index 8fb26e1..990ea08 100644
--- a/src/Snap.Hutao/Snap.Hutao/Factory/Process/ProcessFactory.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Factory/Process/ProcessFactory.cs
@@ -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()
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Island/GameIslandInterop.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Island/GameIslandInterop.cs
index 7d72463..f578cf7 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Island/GameIslandInterop.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Island/GameIslandInterop.cs
@@ -77,21 +77,48 @@ 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(500, 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);
@@ -214,4 +241,4 @@ internal sealed class GameIslandInterop : IGameIslandInterop
}
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/GameProcessFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/GameProcessFactory.cs
index 31f1ef8..0b49ae0 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/GameProcessFactory.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Launching/GameProcessFactory.cs
@@ -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);
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Git/RepositoryAffinity.cs b/src/Snap.Hutao/Snap.Hutao/Service/Git/RepositoryAffinity.cs
index 2be10dd..4b16f92 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Git/RepositoryAffinity.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Git/RepositoryAffinity.cs
@@ -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 Sort(ImmutableArray repositories)
@@ -23,9 +23,11 @@ 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);
+
+ // 对读取值做下限保护,确保排序使用的是非负失败计数
+ int raw = LocalSetting.Get(key, 0);
+ counts[i] = Math.Max(0, raw);
}
Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories));
@@ -42,10 +44,14 @@ 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);
+
+ // 防止整数上溢:当已到达 int.MaxValue 时不再自增
+ if (currentCount < int.MaxValue)
+ {
+ LocalSetting.Set(key, currentCount + 1);
+ }
}
}
@@ -58,10 +64,20 @@ 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);
+
+ // 失败次数不允许小于 0,避免出现负数或整型下溢
+ if (currentCount > 0)
+ {
+ LocalSetting.Set(key, currentCount - 1);
+ }
}
}
-}
\ No newline at end of file
+
+ private static string GetSettingKey(string name, string url)
+ {
+ string urlHash = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
+ return $"{RepositoryAffinityPrefix}{name}::{urlHash}";
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
index e039e01..f112dea 100644
--- a/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
+++ b/src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj
@@ -1,4 +1,4 @@
-
+
WinExe
net10.0-windows10.0.26100.0
@@ -47,12 +47,18 @@
True
Snap.Hutao_TemporaryKey.pfx
SHA256
+
+ false
+
+ None
+
+
+
+
-
-
@@ -241,15 +247,15 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -265,7 +271,7 @@
-
+
@@ -274,10 +280,6 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
all
runtime; build; native; contentfiles; analyzers
diff --git a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/LaunchGamePage.xaml b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/LaunchGamePage.xaml
index 02b3770..d5a68d9 100644
--- a/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/LaunchGamePage.xaml
+++ b/src/Snap.Hutao/Snap.Hutao/UI/Xaml/View/Page/LaunchGamePage.xaml
@@ -553,7 +553,7 @@
-
+
(int)current)
+ if (!map.TryGetValue(key, out object current) || Convert.ToInt32(value) > Convert.ToInt32(current))
{
result.Add(key);
}
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingStorageSetDataFolderOperation.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingStorageSetDataFolderOperation.cs
index 2582af2..c65f5b3 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingStorageSetDataFolderOperation.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingStorageSetDataFolderOperation.cs
@@ -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);
+ }
}
\ No newline at end of file
diff --git a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingStorageViewModel.cs b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingStorageViewModel.cs
index c0e0f6e..7a8760a 100644
--- a/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingStorageViewModel.cs
+++ b/src/Snap.Hutao/Snap.Hutao/ViewModel/Setting/SettingStorageViewModel.cs
@@ -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)
{
diff --git a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DataSigning/SaltConstants.cs b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DataSigning/SaltConstants.cs
index 2fb8996..c34a6fd 100644
--- a/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DataSigning/SaltConstants.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Web/Hoyolab/DataSigning/SaltConstants.cs
@@ -1,11 +1,34 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
namespace Snap.Hutao.Web.Hoyolab.DataSigning;
+///
+/// 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
+///
+
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.
}