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. }