From e55d6ea1c52cd42f6d43a0ffebead0fd98cf614a Mon Sep 17 00:00:00 2001 From: hoshiizumiya Date: Sun, 23 Nov 2025 14:34:45 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E6=94=AF=E6=8C=81=E9=9D=9E=E6=89=93?= =?UTF-8?q?=E5=8C=85=E6=A8=A1=E5=BC=8F=E7=9B=B4=E6=8E=A5=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=94=B9=E4=BC=98=E5=8C=96=E8=AE=BE=E7=BD=AE?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E9=80=BB=E8=BE=91=E9=87=87=E7=94=A8wix=20?= =?UTF-8?q?=E6=89=93=E5=8C=85=20msi=20=E5=AE=89=E8=A3=85=E5=8C=85=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E8=84=B1=E7=A6=BB=E6=B2=99=E7=AE=B1=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=EF=BC=8C=E4=B8=BA=E6=B3=A8=E5=85=A5=E5=8A=9F=E8=83=BD=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E5=A5=A0=E5=AE=9A=E5=9F=BA=E7=A1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 PackageIdentityAdapter 适配打包和非打包模式,支持通过文件系统路径替代 Windows.Storage.ApplicationData。重构 LocalSetting,抽象为 ISettingStorage 接口,支持 JSON 文件存储非打包模式下的设置。改进异常处理和日志记录,增加调试信息和启动诊断日志。隐藏 AppX 打包配置,更新依赖项版本,支持普通管理员模式下的游戏启动和 DLL 注入。补全 SaltConstants 文件,存储数据签名所需的盐值。 已知问题: 注入功能暂不不支持,缺少服务器提供的当前版本注入信息! 没有启用默认管理员权限打开 --- .gitignore | 4 +- .../ExampleComponents.wxs | 9 + .../Snap.Hutao.Installer/Folders.wxs | 3 + .../Snap.Hutao.Installer/Package.en-us.wxl | 8 + .../Snap.Hutao.Installer/Package.wxs | 41 +++ .../Snap.Hutao.Installer.wixproj | 22 ++ src/Snap.Hutao/Snap.Hutao.slnx | 24 +- src/Snap.Hutao/Snap.Hutao/App.xaml.cs | 65 +++- src/Snap.Hutao/Snap.Hutao/Bootstrap.cs | 61 +++- .../ApplicationModel/LimitedAccessFeatures.cs | 14 +- .../PackageIdentityAdapter.cs | 109 ++++++ .../PackageIdentityDiagnostics.cs | 43 +++ .../Core/Diagnostics/HutaoDiagnostics.cs | 16 +- .../Core/Diagnostics/IHutaoDiagnostics.cs | 2 +- .../Snap.Hutao/Core/HutaoRuntime.cs | 104 ++++-- .../Snap.Hutao/Core/InstalledLocation.cs | 27 +- .../Core/LifeCycle/AppActivation.cs | 82 ++++- .../LifeCycle/HutaoActivationArguments.cs | 10 + .../Snap.Hutao/Core/Setting/LocalSetting.cs | 329 ++++++++++++++++-- .../Factory/Process/ProcessFactory.cs | 21 ++ .../Service/Game/Island/GameIslandInterop.cs | 40 ++- .../Game/Launching/GameProcessFactory.cs | 1 + .../Service/Git/RepositoryAffinity.cs | 29 +- src/Snap.Hutao/Snap.Hutao/Snap.Hutao.csproj | 26 +- .../UI/Xaml/View/Page/LaunchGamePage.xaml | 2 +- .../ViewModel/Guide/StaticResource.cs | 4 +- .../SettingStorageSetDataFolderOperation.cs | 33 +- .../Setting/SettingStorageViewModel.cs | 14 +- .../Web/Hoyolab/DataSigning/SaltConstants.cs | 23 ++ 29 files changed, 1037 insertions(+), 129 deletions(-) create mode 100644 src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs create mode 100644 src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs create mode 100644 src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl create mode 100644 src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs create mode 100644 src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityAdapter.cs create mode 100644 src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityDiagnostics.cs diff --git a/.gitignore b/.gitignore index 7f4b36a..268eb8c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,6 @@ src/Snap.Hutao/Snap.Hutao/Generated Files/ tools/ -src/Snap.Hutao/Snap.Hutao/AppPackages \ No newline at end of file +src/Snap.Hutao/Snap.Hutao/AppPackages +/src/Snap.Hutao/Snap.Hutao.Installer/obj +/src/Snap.Hutao/Snap.Hutao.Installer/bin/x64/Release/en-US diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs b/src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs new file mode 100644 index 0000000..4ddb9b0 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs @@ -0,0 +1,9 @@ + + + + + + + + + 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..0cfa788 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file 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..3025a9c --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj @@ -0,0 +1,22 @@ + + + ICE03;ICE60 + x64 + + + + + + + + + MainAppComponents + INSTALLFOLDER + true + true + true + + + + + \ No newline at end of file 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..6dd2206 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityAdapter.cs @@ -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; + +/// +/// 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=DGPStudio"; + } +} 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..28cbf40 --- /dev/null +++ b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityDiagnostics.cs @@ -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; + +/// +/// 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..87eb72f 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,47 @@ internal sealed class GameIslandInterop : IGameIslandInterop { nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle(); InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea); + if (!resume) { - if (context.Process is not FullTrustProcess fullTrustProcess) - { - throw HutaoException.InvalidOperation("Process is not full trust"); - } - ArgumentException.ThrowIfNullOrEmpty(islandPath); if (!File.Exists(islandPath)) { throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists); } - fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath)); - fullTrustProcess.ResumeMainThread(); + // Support both FullTrust and normal admin mode + if (context.Process is FullTrustProcess fullTrustProcess) + { + // Use FullTrust process for injection (suspended process) + fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath)); + fullTrustProcess.ResumeMainThread(); + } + else + { + // Use native injection for normal admin mode + // The process was already started by CreateUsingShellExecuteRunAs + // Just inject the DLL into the running process + try + { + // Wait a bit for process to initialize + // await Task.Delay(5000, token).ConfigureAwait(false); + + // Inject using RemoteThread + DllInjectionUtilities.InjectUsingRemoteThread(islandPath, context.Process.Id); + } + catch (Exception ex) + { + // Log the injection failure but don't crash - game can still run + SentrySdk.AddBreadcrumb( + $"Island DLL injection failed: {ex.Message}", + category: "island.injection", + level: Sentry.BreadcrumbLevel.Error); + + // Re-throw to let the caller handle it + throw HutaoException.Throw($"Island DLL 注入失败: {ex.Message}", ex); + } + } } await PeriodicUpdateIslandEnvironmentAsync(context, handle, token).ConfigureAwait(false); 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..69f5cda 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,8 @@ internal static class RepositoryAffinity for (int i = 0; i < repositories.Length; i++) { GitRepository repository = repositories[i]; - ApplicationDataContainer container = RepositoryContainer.CreateContainer(repository.Name, ApplicationDataCreateDisposition.Always); - string key = Hash.ToHexString(HashAlgorithmName.SHA256, repository.HttpsUrl.OriginalString.ToUpperInvariant()); - counts[i] = container.Values[key] is int c ? c : 0; + string key = GetSettingKey(repository.Name, repository.HttpsUrl.OriginalString); + counts[i] = LocalSetting.Get(key, 0); } Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories)); @@ -42,10 +41,9 @@ internal static class RepositoryAffinity { lock (SyncRoot) { - ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always); - string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant()); - object box = container.Values[key]; - container.Values[key] = box is int count ? unchecked(count + 1) : 1; + string key = GetSettingKey(name, url); + int currentCount = LocalSetting.Get(key, 0); + LocalSetting.Set(key, unchecked(currentCount + 1)); } } @@ -58,10 +56,15 @@ internal static class RepositoryAffinity { lock (SyncRoot) { - ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always); - string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant()); - object box = container.Values[key]; - container.Values[key] = box is int count ? unchecked(count - 1) : 0; + string key = GetSettingKey(name, url); + int currentCount = LocalSetting.Get(key, 0); + LocalSetting.Set(key, unchecked(currentCount - 1)); } } + + private static string GetSettingKey(string name, string url) + { + string urlHash = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant()); + return $"{RepositoryAffinityPrefix}{name}::{urlHash}"; + } } \ No newline at end of file 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. } From 3f50507490c774204592c9ed90f4f98ff076b2d2 Mon Sep 17 00:00:00 2001 From: hoshiizumiya <63837495+hoshiizumiya@users.noreply.github.com> Date: Sun, 23 Nov 2025 19:37:46 +0800 Subject: [PATCH 02/10] Delete src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 删除 wix 示例配置代码 --- .../Snap.Hutao.Installer/ExampleComponents.wxs | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs b/src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs deleted file mode 100644 index 4ddb9b0..0000000 --- a/src/Snap.Hutao/Snap.Hutao.Installer/ExampleComponents.wxs +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - From 1039623cbf99ba94f46dd5f3c29c8366cacf325c Mon Sep 17 00:00:00 2001 From: hoshiizumiya <63837495+hoshiizumiya@users.noreply.github.com> Date: Sun, 23 Nov 2025 20:10:02 +0800 Subject: [PATCH 03/10] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=AE=89=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加桌面图标,修正注册表安装目标 --- .../Snap.Hutao.Installer/Package.wxs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs b/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs index 0cfa788..86f76c2 100644 --- a/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs +++ b/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs @@ -11,6 +11,7 @@ + @@ -22,11 +23,24 @@ + + + + + + + + + + - - + - \ No newline at end of file + From e045413ac1a91a807b0dffca3712e66f19bd3df9 Mon Sep 17 00:00:00 2001 From: hoshiizumiya <63837495+hoshiizumiya@users.noreply.github.com> Date: Sun, 23 Nov 2025 20:17:47 +0800 Subject: [PATCH 04/10] Update HarvestDirectory path for project configuration --- .../Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj b/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj index 3025a9c..1ad4b00 100644 --- a/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj +++ b/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj @@ -9,7 +9,7 @@ - + MainAppComponents INSTALLFOLDER true @@ -19,4 +19,4 @@ - \ No newline at end of file + From d318dcbfd0ac0a95e38398b335b97858830fa1cf Mon Sep 17 00:00:00 2001 From: hoshiizumiya <63837495+hoshiizumiya@users.noreply.github.com> Date: Sun, 23 Nov 2025 20:46:54 +0800 Subject: [PATCH 05/10] Update copyright information in PackageIdentityAdapter.cs --- .../Core/ApplicationModel/PackageIdentityAdapter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityAdapter.cs b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityAdapter.cs index 6dd2206..7ccd7cb 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityAdapter.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityAdapter.cs @@ -1,4 +1,4 @@ -// Copyright (c) DGP Studio. All rights reserved. +// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved. // Licensed under the MIT license. using System.Diagnostics; @@ -104,6 +104,6 @@ internal static class PackageIdentityAdapter } // Unpackaged: use a fallback - return "CN=DGPStudio"; + return "CN=Millennium-Science-Technology-R-D-Inst"; } } From a7bb931ea5ae05a07469b5b2e51d38cef242b08b Mon Sep 17 00:00:00 2001 From: hoshiizumiya <63837495+hoshiizumiya@users.noreply.github.com> Date: Sun, 23 Nov 2025 20:47:40 +0800 Subject: [PATCH 06/10] Update copyright notice in PackageIdentityDiagnostics.cs --- .../Core/ApplicationModel/PackageIdentityDiagnostics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityDiagnostics.cs b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityDiagnostics.cs index 28cbf40..2397128 100644 --- a/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityDiagnostics.cs +++ b/src/Snap.Hutao/Snap.Hutao/Core/ApplicationModel/PackageIdentityDiagnostics.cs @@ -1,4 +1,4 @@ -// Copyright (c) DGP Studio. All rights reserved. +// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved. // Licensed under the MIT license. using System.Diagnostics; From 5f196253b33c4aa3d6cfb1bb15fec1ecdf1738fd Mon Sep 17 00:00:00 2001 From: hoshiizumiya <63837495+hoshiizumiya@users.noreply.github.com> Date: Sun, 23 Nov 2025 21:24:10 +0800 Subject: [PATCH 07/10] Enhance failure count handling in RepositoryAffinity Add lower bounds protection for failure counts to prevent negative values and integer underflow. --- .../Service/Git/RepositoryAffinity.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Git/RepositoryAffinity.cs b/src/Snap.Hutao/Snap.Hutao/Service/Git/RepositoryAffinity.cs index 69f5cda..4b16f92 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Git/RepositoryAffinity.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Git/RepositoryAffinity.cs @@ -24,7 +24,10 @@ internal static class RepositoryAffinity { GitRepository repository = repositories[i]; string key = GetSettingKey(repository.Name, repository.HttpsUrl.OriginalString); - counts[i] = LocalSetting.Get(key, 0); + + // 对读取值做下限保护,确保排序使用的是非负失败计数 + int raw = LocalSetting.Get(key, 0); + counts[i] = Math.Max(0, raw); } Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories)); @@ -43,7 +46,12 @@ internal static class RepositoryAffinity { string key = GetSettingKey(name, url); int currentCount = LocalSetting.Get(key, 0); - LocalSetting.Set(key, unchecked(currentCount + 1)); + + // 防止整数上溢:当已到达 int.MaxValue 时不再自增 + if (currentCount < int.MaxValue) + { + LocalSetting.Set(key, currentCount + 1); + } } } @@ -58,7 +66,12 @@ internal static class RepositoryAffinity { string key = GetSettingKey(name, url); int currentCount = LocalSetting.Get(key, 0); - LocalSetting.Set(key, unchecked(currentCount - 1)); + + // 失败次数不允许小于 0,避免出现负数或整型下溢 + if (currentCount > 0) + { + LocalSetting.Set(key, currentCount - 1); + } } } @@ -67,4 +80,4 @@ internal static class RepositoryAffinity string urlHash = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant()); return $"{RepositoryAffinityPrefix}{name}::{urlHash}"; } -} \ No newline at end of file +} From 63c4792e009419844412107dce218cd7fad2f306 Mon Sep 17 00:00:00 2001 From: hoshiizumiya <63837495+hoshiizumiya@users.noreply.github.com> Date: Sun, 23 Nov 2025 21:29:19 +0800 Subject: [PATCH 08/10] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B3=A8=E6=84=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Snap.Hutao/Service/Game/Island/GameIslandInterop.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 87eb72f..f578cf7 100644 --- a/src/Snap.Hutao/Snap.Hutao/Service/Game/Island/GameIslandInterop.cs +++ b/src/Snap.Hutao/Snap.Hutao/Service/Game/Island/GameIslandInterop.cs @@ -101,7 +101,8 @@ internal sealed class GameIslandInterop : IGameIslandInterop try { // Wait a bit for process to initialize - // await Task.Delay(5000, token).ConfigureAwait(false); + // await Task.Delay(500, token).ConfigureAwait(false); + // ⚠️此处需要更多调查 // Inject using RemoteThread DllInjectionUtilities.InjectUsingRemoteThread(islandPath, context.Process.Id); @@ -240,4 +241,4 @@ internal sealed class GameIslandInterop : IGameIslandInterop } } } -} \ No newline at end of file +} From cc926e4352cac6b04bd73075932f8af50528fceb Mon Sep 17 00:00:00 2001 From: wangdage12 <124357765+wangdage12@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:17:01 +0800 Subject: [PATCH 09/10] =?UTF-8?q?=E6=8C=87=E5=AE=9ATargetFramework?= =?UTF-8?q?=E5=80=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj b/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj index 1ad4b00..904d477 100644 --- a/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj +++ b/src/Snap.Hutao/Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj @@ -2,6 +2,7 @@ ICE03;ICE60 x64 + net10.0-windows10.0.26100.0 From 2cc2cc80ac710a0840bdc1e16c7481084ca6d9be Mon Sep 17 00:00:00 2001 From: wangdage12 <124357765+wangdage12@users.noreply.github.com> Date: Wed, 26 Nov 2025 19:18:01 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E4=BF=AE=E5=A4=8DKeyPath=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Snap.Hutao.Installer/Package.wxs | 110 +++++++++++------- 1 file changed, 67 insertions(+), 43 deletions(-) diff --git a/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs b/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs index 86f76c2..6567b23 100644 --- a/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs +++ b/src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs @@ -1,55 +1,79 @@ - + - - + + - - - - - - + + - - - - + + + + + - - - + + + + + - - + + + - - - + + - - - + + + - - - + - + + + + - - - + + + + + + + + + + + +