diff --git a/CollapseLauncher.slnx b/CollapseLauncher.slnx
index bfdd010376..04fa93e882 100644
--- a/CollapseLauncher.slnx
+++ b/CollapseLauncher.slnx
@@ -53,6 +53,10 @@
+
+
+
+
diff --git a/CollapseLauncher/App.xaml.cs b/CollapseLauncher/App.xaml.cs
index 327484fb9c..fdbbb153fb 100644
--- a/CollapseLauncher/App.xaml.cs
+++ b/CollapseLauncher/App.xaml.cs
@@ -137,12 +137,6 @@ protected override void OnLaunched(LaunchActivatedEventArgs args)
{
ImageLoaderHelper.InitWaifu2X();
}
-
- // Initialize support for MagicScaler's WebP decoding
- CodecManager.Configure(codecs =>
- {
- codecs.UseLibwebp();
- });
}
catch (Exception ex)
{
diff --git a/CollapseLauncher/Assets/Images/GameBackground/genshin.avif b/CollapseLauncher/Assets/Images/GameBackground/genshin.avif
new file mode 100644
index 0000000000..fca3d6690f
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/genshin.avif differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/genshin.webp b/CollapseLauncher/Assets/Images/GameBackground/genshin.webp
deleted file mode 100644
index 2b38257579..0000000000
Binary files a/CollapseLauncher/Assets/Images/GameBackground/genshin.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/honkai.avif b/CollapseLauncher/Assets/Images/GameBackground/honkai.avif
new file mode 100644
index 0000000000..478fdfa9d6
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/honkai.avif differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/honkai.webp b/CollapseLauncher/Assets/Images/GameBackground/honkai.webp
deleted file mode 100644
index 8d0ed46ea8..0000000000
Binary files a/CollapseLauncher/Assets/Images/GameBackground/honkai.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/starrail.avif b/CollapseLauncher/Assets/Images/GameBackground/starrail.avif
new file mode 100644
index 0000000000..54f1e52ec4
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/starrail.avif differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/starrail.webp b/CollapseLauncher/Assets/Images/GameBackground/starrail.webp
deleted file mode 100644
index 149b046deb..0000000000
Binary files a/CollapseLauncher/Assets/Images/GameBackground/starrail.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/zzz.avif b/CollapseLauncher/Assets/Images/GameBackground/zzz.avif
new file mode 100644
index 0000000000..df987f585e
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GameBackground/zzz.avif differ
diff --git a/CollapseLauncher/Assets/Images/GameBackground/zzz.webp b/CollapseLauncher/Assets/Images/GameBackground/zzz.webp
deleted file mode 100644
index d1f3f1a198..0000000000
Binary files a/CollapseLauncher/Assets/Images/GameBackground/zzz.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.avif b/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.avif
new file mode 100644
index 0000000000..a5868121d8
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.avif differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.png b/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.png
deleted file mode 100644
index ee2504b7d2..0000000000
Binary files a/CollapseLauncher/Assets/Images/GamePoster/poster_genshin.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.avif b/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.avif
new file mode 100644
index 0000000000..e4a7e00469
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.avif differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.png b/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.png
deleted file mode 100644
index 3ba48393b9..0000000000
Binary files a/CollapseLauncher/Assets/Images/GamePoster/poster_honkai.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.avif b/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.avif
new file mode 100644
index 0000000000..6d59581e3c
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.avif differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.png b/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.png
deleted file mode 100644
index 9d82a6712f..0000000000
Binary files a/CollapseLauncher/Assets/Images/GamePoster/poster_starrail.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.avif b/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.avif
new file mode 100644
index 0000000000..592af416a7
Binary files /dev/null and b/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.avif differ
diff --git a/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.png b/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.png
deleted file mode 100644
index 389af86feb..0000000000
Binary files a/CollapseLauncher/Assets/Images/GamePoster/poster_zzz.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground.png b/CollapseLauncher/Assets/Images/PageBackground/StartupBackground.png
deleted file mode 100644
index 4cad046e38..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.avif b/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.avif
new file mode 100644
index 0000000000..1514071f5f
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.png b/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.png
deleted file mode 100644
index 25f0a2a092..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/StartupBackground2.png and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_genshin.avif b/CollapseLauncher/Assets/Images/PageBackground/default_genshin.avif
new file mode 100644
index 0000000000..82415e3a9f
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/default_genshin.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_genshin.webp b/CollapseLauncher/Assets/Images/PageBackground/default_genshin.webp
deleted file mode 100644
index 30bbfa05aa..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/default_genshin.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_honkai.avif b/CollapseLauncher/Assets/Images/PageBackground/default_honkai.avif
new file mode 100644
index 0000000000..fcda9bf2a0
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/default_honkai.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_honkai.webp b/CollapseLauncher/Assets/Images/PageBackground/default_honkai.webp
deleted file mode 100644
index 6953e43a31..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/default_honkai.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_starrail.avif b/CollapseLauncher/Assets/Images/PageBackground/default_starrail.avif
new file mode 100644
index 0000000000..241acde4b3
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/default_starrail.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_starrail.webp b/CollapseLauncher/Assets/Images/PageBackground/default_starrail.webp
deleted file mode 100644
index c696b35089..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/default_starrail.webp and /dev/null differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_zzz.avif b/CollapseLauncher/Assets/Images/PageBackground/default_zzz.avif
new file mode 100644
index 0000000000..6a325a280e
Binary files /dev/null and b/CollapseLauncher/Assets/Images/PageBackground/default_zzz.avif differ
diff --git a/CollapseLauncher/Assets/Images/PageBackground/default_zzz.webp b/CollapseLauncher/Assets/Images/PageBackground/default_zzz.webp
deleted file mode 100644
index 0e33010e68..0000000000
Binary files a/CollapseLauncher/Assets/Images/PageBackground/default_zzz.webp and /dev/null differ
diff --git a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs
index bd524472c6..415c66c2c0 100644
--- a/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs
+++ b/CollapseLauncher/Classes/AnimatedVisuals/Lottie/NewLogoTitleIntro.cs
@@ -43,6 +43,9 @@
// | Gradient stops | 6 |
// | CompositionVisualSurface | 42 |
// ------------------------------------
+
+using CollapseLauncher.GameManagement.ImageBackground;
+using CollapseLauncher.Helper.Image;
using Hi3Helper.Shared.Region;
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.Effects;
@@ -55,6 +58,7 @@
using System.ComponentModel;
using System.IO;
using System.Numerics;
+using System.Threading.Tasks;
using Windows.Foundation;
using Windows.UI;
@@ -156,9 +160,9 @@ void EnsureImageLoadingStarted()
if (!_isImageLoadingStarted)
{
var eventHandler = new TypedEventHandler(HandleLoadCompleted);
- _image_image_0 = LoadedImageSurface.StartLoadFromStream(File.OpenRead(Path.Combine(LauncherConfig.AppExecutableDir, @"Assets\CollapseLauncherLogo.png")).AsRandomAccessStream());
+ _image_image_0 = LoadedImageSurface.StartLoadFromStream(File.OpenRead(Path.Combine(LauncherConfig.AppExecutableDir, @"Assets\CollapseLauncherLogo.png")).AsRandomAccessStream());
_image_image_0.LoadCompleted += eventHandler;
- _isImageLoadingStarted = true;
+ _isImageLoadingStarted = true;
}
}
diff --git a/CollapseLauncher/Classes/DiscordPresence/DiscordActivityType.cs b/CollapseLauncher/Classes/DiscordPresence/DiscordActivityType.cs
new file mode 100644
index 0000000000..4a05430ec8
--- /dev/null
+++ b/CollapseLauncher/Classes/DiscordPresence/DiscordActivityType.cs
@@ -0,0 +1,15 @@
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.DiscordPresence;
+
+public enum DiscordActivityType
+{
+ None,
+ Idle,
+ Play,
+ Update,
+ Repair,
+ Cache,
+ GameSettings,
+ AppSettings
+}
diff --git a/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs b/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs
deleted file mode 100644
index d4f172e4e0..0000000000
--- a/CollapseLauncher/Classes/DiscordPresence/DiscordPresenceManager.cs
+++ /dev/null
@@ -1,476 +0,0 @@
-using CollapseLauncher.Helper;
-using CollapseLauncher.Helper.Metadata;
-using CollapseLauncher.Helper.Update;
-using CollapseLauncher.Plugins;
-using DiscordRPC;
-using DiscordRPC.Entities;
-using DiscordRPC.Message;
-using Hi3Helper;
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
-using System.Threading.Tasks.Dataflow;
-using static Hi3Helper.Shared.Region.LauncherConfig;
-// ReSharper disable PartialTypeWithSinglePart
-// ReSharper disable StringLiteralTypo
-// ReSharper disable SwitchStatementHandlesSomeKnownEnumValuesWithDefault
-#pragma warning disable IDE0130
-
-#nullable enable
-namespace CollapseLauncher.DiscordPresence
-{
- #region Enums
-
- public enum ActivityType
- {
- None,
- Idle,
- Play,
- Update,
- Repair,
- Cache,
- GameSettings,
- AppSettings
- }
-
- #endregion
-
- public sealed partial class DiscordPresenceManager : IDisposable
- {
- #region Properties
-
- public bool IsRpcEnabled
- {
- get => field = GetAppConfigValue("EnableDiscordRPC");
- set
- {
- if (field == value) return;
- field = value;
-
- SetAndSaveConfigValue("EnableDiscordRPC", value);
- if (value) SetupPresence(null);
- else DisablePresence();
- }
- }
-
- private const string CollapseLogoExt = "https://collapselauncher.com/img/logo@2x.webp";
-
- private DiscordRpcClient? _client;
-
- private RichPresence? _presence;
- private ActivityType _activityType;
- private DateTime? _lastPlayTime;
- private bool _firstTimeConnect = true;
- private readonly ActionBlock _presenceUpdateQueue;
-
- private bool _cachedIsIdleEnabled = true;
-
- public bool IdleEnabled
- {
- get
- {
- bool value = GetAppConfigValue("EnableDiscordIdleStatus");
- _cachedIsIdleEnabled = value;
- return value;
- }
- set
- {
- SetAndSaveConfigValue("EnableDiscordIdleStatus", value);
- _cachedIsIdleEnabled = value;
- }
- }
-
- #endregion
-
- public DiscordPresenceManager(bool initialStart = true)
- {
- _presenceUpdateQueue = new ActionBlock(_ => _client?.SetPresence(_presence),
- new ExecutionDataflowBlockOptions
- {
- MaxMessagesPerTask = 1,
- MaxDegreeOfParallelism = 1,
- EnsureOrdered = true
- });
-
- if (!initialStart)
- {
- return;
- }
-
- // Prepare idle cached setting
- Logger.LogWriteLine($"Doing initial start for Discord RPC!\r\n\tIdle status : {IdleEnabled}",
- LogType.Scheme);
- }
-
- // Deconstruct and dispose unmanaged resources
- ~DiscordPresenceManager()
- {
- Dispose();
- }
-
- public void Dispose()
- {
- // Dispose Discord RPC client
- DisablePresence();
-
- // Suppress the GC from finalization
- GC.SuppressFinalize(this);
- }
-
- private void EnablePresence(ulong applicationId)
- {
- if (!IsRpcEnabled) return;
- _firstTimeConnect = true;
-
- // Flush and dispose the session
- DisablePresence();
-
- // Initialize Discord RPC client
- _client = new DiscordRpcClient(applicationId.ToString(), ILoggerHelper.GetILogger("DiscordRPC"));
-
- _client.OnReady += OnReady;
- _client.OnPresenceUpdate += OnPresenceUpdate;
-
- if (!_client.Initialize())
- {
- Logger.LogWriteLine("Error initializing Discord Presence.", LogType.Warning, true);
- return;
- }
-
- Logger.LogWriteLine("Discord Presence is Enabled!");
- }
-
- private void OnReady(object? sender, ReadyMessage? msg)
- {
- Logger.LogWriteLine($"Connected to Discord with user {msg?.User?.Username}");
- if (!_firstTimeConnect)
- {
- // Restart Discord RPC client
- _firstTimeConnect = true;
- SetupPresence(null);
- }
- else
- {
- // Restore our last activity
- if (!(!_cachedIsIdleEnabled &&
- _activityType is ActivityType.Idle or ActivityType.None))
- {
- SetActivity(_activityType);
- }
-
- _firstTimeConnect = false;
- }
- }
-
- private static void OnPresenceUpdate(object? sender, PresenceMessage? msg)
- {
- if (msg?.Presence == null)
- {
- Logger.LogWriteLine("Activity cleared!");
- }
- else
- {
- Logger.LogWriteLine(msg.Presence.State == null
- ? $"Activity updated! => {msg.Presence.Details}"
- : $"Activity updated! => {msg.Presence.Details} - {msg.Presence.State}");
- }
- }
-
- public void DisablePresence()
- {
- _client?.SetPresence(null);
- _client?.Dispose();
- _client = null;
- }
-
- private static ulong GetDiscordPresenceId(PresetConfig presetConfig)
- {
- return presetConfig.GameName switch
- {
- "Honkai: Star Rail" => AppDiscordApplicationIDHsr,
- "Honkai Impact 3rd" => AppDiscordApplicationIDHi3,
- "Genshin Impact" => AppDiscordApplicationIDGi,
- "Zenless Zone Zero" => AppDiscordApplicationIDZzz,
- _ => TryGetPresenceFromPlugin(presetConfig)
- };
-
- static ulong TryGetPresenceFromPlugin(PresetConfig presetConfig)
- {
- if (presetConfig is not PluginPresetConfigWrapper { DiscordPresenceContext : { IsFeatureAvailable: true } discordContext } ||
- discordContext.PresenceId == 0)
- {
- return AppDiscordApplicationID; // Default
- }
-
- return discordContext.PresenceId;
- }
- }
-
- public void SetupPresence(PresetConfig? presetConfig)
- {
- bool isGameStatusEnabled = GetAppConfigValue("EnableDiscordGameStatus");
- if (!IsRpcEnabled || !isGameStatusEnabled) return;
-
- string gameTitle = MetadataHelper.CurrentGameTitleName;
- string gameRegion = MetadataHelper.CurrentGameRegionName;
-
- if (presetConfig == null &&
- !MetadataHelper.TryGetGameConfig(gameTitle, gameRegion, out presetConfig))
- {
- return;
- }
-
- if (GetDiscordPresenceId(presetConfig) is var presenceId && presenceId == 0)
- {
- Logger.LogWriteLine("Discord Presence (Unknown Game)", LogType.Error, true);
- }
-
- EnablePresence(presenceId);
- }
-
- public void SetActivity(ActivityType activity, DateTime? activityOffset = null)
- {
- if (!IsRpcEnabled) return;
-
- //_lastAttemptedActivityType = activity;
- _activityType = activity;
-
- switch (activity)
- {
- case ActivityType.Play:
- {
- bool isGameStatusEnabled = GetAppConfigValue("EnableDiscordGameStatus").ToBool();
- BuildActivityGameStatus((isGameStatusEnabled ? Locale.Current.Lang?._Misc?.DiscordRP_InGame : Locale.Current.Lang?._Misc?.DiscordRP_Play) ?? "",
- isGameStatusEnabled, activityOffset);
- break;
- }
- case ActivityType.Update:
- {
- bool isGameStatusEnabled = GetAppConfigValue("EnableDiscordGameStatus").ToBool();
- BuildActivityGameStatus(Locale.Current.Lang?._Misc?.DiscordRP_Update ?? "", isGameStatusEnabled);
- break;
- }
- case ActivityType.Repair:
- BuildActivityAppStatus(Locale.Current.Lang?._Misc?.DiscordRP_Repair ?? "");
- break;
- case ActivityType.Cache:
- BuildActivityAppStatus(Locale.Current.Lang?._Misc?.DiscordRP_Cache ?? "");
- break;
- case ActivityType.GameSettings:
- BuildActivityAppStatus(Locale.Current.Lang?._Misc?.DiscordRP_GameSettings ?? "");
- break;
- case ActivityType.AppSettings:
- BuildActivityAppStatus(Locale.Current.Lang?._Misc?.DiscordRP_AppSettings ?? "");
- break;
- case ActivityType.Idle:
- _lastPlayTime = null;
- if (_cachedIsIdleEnabled)
- {
- BuildActivityAppStatus(Locale.Current.Lang?._Misc?.DiscordRP_Idle ?? "");
- }
- else
- {
- _presence = null; // Clear presence
- }
-
- break;
- default:
- _presence = new RichPresence
- {
- Details = Locale.Current.Lang?._Misc?.DiscordRP_Default,
- Assets = new Assets
- {
- LargeImageKey = "launcher-logo-new",
- LargeImageText =
- $"Collapse Launcher v{LauncherUpdateHelper.LauncherCurrentVersionString} {(IsPreview ? "Preview" : "Stable")}"
- },
- Timestamps = null!
- };
- break;
- }
-
- UpdateActivity();
- }
-
- private void BuildActivityGameStatus(string activityName, bool isGameStatusEnabled, DateTime? activityOffset = null)
- {
- string curGameName = MetadataHelper.CurrentGameTitleName;
- string curGameRegion = MetadataHelper.CurrentGameRegionName;
-
- if (string.IsNullOrEmpty(curGameName) || string.IsNullOrEmpty(curGameRegion) ||
- !MetadataHelper.TryGetGameConfig(curGameName, curGameRegion, out PresetConfig? presetConfig))
- return;
-
- string curGameNameTranslate = MetadataHelper.GetTranslatedTitle(curGameName);
- string curGameRegionTranslate = MetadataHelper.GetTranslatedRegion(curGameRegion);
-
- if (TryBuildActivityGameStatusFromPlugin(activityName,
- curGameNameTranslate,
- curGameRegionTranslate,
- isGameStatusEnabled,
- activityOffset,
- presetConfig,
- out _presence))
- {
- return;
- }
-
- _presence = new RichPresence
- {
- Details = $"{activityName} {(!isGameStatusEnabled ? curGameNameTranslate : null)}",
- State = $"{Locale.Current.Lang?._Misc?.DiscordRP_Region} {curGameRegionTranslate}",
- Assets = new Assets
- {
- LargeImageKey = $"game-{presetConfig.GameType.ToString().ToLower()}-logo",
- LargeImageText = $"{curGameNameTranslate} - {curGameRegionTranslate}",
- SmallImageKey = "launcher-logo-new",
- SmallImageText = $"Collapse Launcher v{LauncherUpdateHelper.LauncherCurrentVersionString} "
- + $"{(IsPreview ? "Preview" : "Stable")}"
- },
- Timestamps = new Timestamps
- {
- Start = GetCachedStartPlayTime(activityOffset)
- }
- };
- }
-
- private bool TryBuildActivityGameStatusFromPlugin(
- string activityName,
- string? translatedGameName,
- string? translatedRegionName,
- bool isGameStatusEnabled,
- DateTime? activityOffset,
- PresetConfig presetConfig,
- [NotNullWhen(true)] out RichPresence? presence)
- {
- Unsafe.SkipInit(out presence);
-
- if (presetConfig is not PluginPresetConfigWrapper asPluginPresetConfig ||
- !asPluginPresetConfig.DiscordPresenceContext.IsFeatureAvailable)
- {
- return false;
- }
-
- string? largeIconUrl = asPluginPresetConfig.DiscordPresenceContext.LargeIconUrl;
- string? largeIconTooltip = asPluginPresetConfig.DiscordPresenceContext.LargeIconTooltip;
- string? smallIconUrl = asPluginPresetConfig.DiscordPresenceContext.SmallIconUrl;
- string? smallIconTooltip = asPluginPresetConfig.DiscordPresenceContext.SmallIconTooltip;
-
- presence = new RichPresence
- {
- Details = $"{activityName} {(!isGameStatusEnabled ? translatedGameName : null)}",
- State = $"{Locale.Current.Lang?._Misc?.DiscordRP_Region} {translatedRegionName}",
- Assets = new Assets
- {
- LargeImageKey = largeIconUrl ?? CollapseLogoExt,
- LargeImageText = largeIconTooltip ?? $"{translatedGameName} - {translatedRegionName}",
- SmallImageKey = smallIconUrl ?? CollapseLogoExt,
- SmallImageText = smallIconTooltip ??
- $"Collapse Launcher v{LauncherUpdateHelper.LauncherCurrentVersionString} "
- + $"{(IsPreview ? "Preview" : "Stable")}"
- },
- Timestamps = new Timestamps
- {
- Start = GetCachedStartPlayTime(activityOffset)
- }
- };
-
- return true;
- }
-
- private DateTime GetCachedStartPlayTime(DateTime? activityOffset)
- {
- _lastPlayTime ??= activityOffset;
- _lastPlayTime ??= DateTime.UtcNow;
- return _lastPlayTime.Value;
- }
-
- private void BuildActivityAppStatus(string activityName)
- {
- string curGameName = MetadataHelper.CurrentGameTitleName;
- string curGameRegion = MetadataHelper.CurrentGameRegionName;
-
- if (string.IsNullOrEmpty(curGameName) || string.IsNullOrEmpty(curGameRegion) ||
- !MetadataHelper.TryGetGameConfig(curGameName, curGameRegion, out PresetConfig? presetConfig))
- return;
-
- string curGameNameTranslate = MetadataHelper.GetTranslatedTitle(curGameName);
- string curGameRegionTranslate = MetadataHelper.GetTranslatedRegion(curGameRegion);
-
- if (TryBuildActivityAppStatusFromPlugin(activityName,
- curGameNameTranslate,
- curGameRegionTranslate,
- presetConfig,
- out _presence))
- {
- return;
- }
-
- _presence = new RichPresence
- {
- Details = activityName,
- State = $"{Locale.Current.Lang?._Misc?.DiscordRP_Region} {curGameRegionTranslate}",
- Assets = new Assets
- {
- LargeImageKey = $"game-{presetConfig.GameType.ToString().ToLower()}-logo",
- LargeImageText = curGameNameTranslate,
- SmallImageKey = "launcher-logo-new",
- SmallImageText = $"Collapse Launcher v{LauncherUpdateHelper.LauncherCurrentVersionString} "
- + $"{(IsPreview ? "Preview" : "Stable")}"
- },
- Timestamps = null!
- };
- }
-
- private static bool TryBuildActivityAppStatusFromPlugin(
- string activityName,
- string? translatedGameName,
- string? translatedRegionName,
- PresetConfig presetConfig,
- [NotNullWhen(true)] out RichPresence? presence)
- {
- Unsafe.SkipInit(out presence);
-
- if (presetConfig is not PluginPresetConfigWrapper asPluginPresetConfig ||
- !asPluginPresetConfig.DiscordPresenceContext.IsFeatureAvailable)
- {
- return false;
- }
-
- string? largeIconUrl = asPluginPresetConfig.DiscordPresenceContext.LargeIconUrl;
- string? largeIconTooltip = asPluginPresetConfig.DiscordPresenceContext.LargeIconTooltip;
- string? smallIconUrl = asPluginPresetConfig.DiscordPresenceContext.SmallIconUrl;
- string? smallIconTooltip = asPluginPresetConfig.DiscordPresenceContext.SmallIconTooltip;
-
- presence = new RichPresence
- {
- Details = activityName,
- State = $"{Locale.Current.Lang?._Misc?.DiscordRP_Region} {translatedRegionName}",
- Assets = new Assets
- {
- LargeImageKey = largeIconUrl ?? CollapseLogoExt,
- LargeImageText = largeIconTooltip ?? translatedGameName,
- SmallImageKey = smallIconUrl ?? CollapseLogoExt,
- SmallImageText = smallIconTooltip ??
- $"Collapse Launcher v{LauncherUpdateHelper.LauncherCurrentVersionString} "
- + $"{(IsPreview ? "Preview" : "Stable")}"
- },
- Timestamps = null!
- };
-
- return true;
- }
-
- private void UpdateActivity()
- {
- try
- {
- _presenceUpdateQueue.Post(_presence);
- }
- catch (Exception ex)
- {
- Logger.LogWriteLine($"Error when updating Discord Presence Activity\r\n{ex}", LogType.Error, true);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/DiscordPresence/DiscordRpcManager.cs b/CollapseLauncher/Classes/DiscordPresence/DiscordRpcManager.cs
new file mode 100644
index 0000000000..9c3764d42b
--- /dev/null
+++ b/CollapseLauncher/Classes/DiscordPresence/DiscordRpcManager.cs
@@ -0,0 +1,422 @@
+using CollapseLauncher.Helper;
+using CollapseLauncher.Helper.Metadata;
+using CollapseLauncher.Helper.Update;
+using CollapseLauncher.Plugins;
+using DiscordRPC;
+using DiscordRPC.Entities;
+using DiscordRPC.Message;
+using Hi3Helper;
+using Hi3Helper.LocaleSourceGen;
+using Hi3Helper.Shared.Region;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Collections.Concurrent;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Channels;
+
+#pragma warning disable IDE0130
+
+#nullable enable
+namespace CollapseLauncher.DiscordPresence;
+
+public partial class DiscordRpcManager : IDisposable
+{
+ public bool IsDisposed;
+
+ public DiscordRpcClient? Client;
+ public readonly Thread PresenceSetThread;
+ public readonly Channel PresenceSetChannel;
+
+ private ulong _lastPresenceId = LauncherConfig.AppDiscordApplicationID;
+ private DiscordActivityType _lastActivityStatus;
+
+ private readonly EventWaitHandle _isReadyWaitHandle = new(false, EventResetMode.ManualReset);
+
+ public bool IsGameStatusEnabled
+ {
+ get => LauncherConfig.GetAppConfigValue("EnableDiscordGameStatus");
+ set
+ {
+ LauncherConfig.SetAndSaveConfigValue("EnableDiscordGameStatus", value);
+ SetActivity(_lastActivityStatus); // Refresh activity status to the last one
+ }
+ }
+
+ public bool IsShowOnIdle
+ {
+ get => LauncherConfig.GetAppConfigValue("EnableDiscordIdleStatus");
+ set
+ {
+ LauncherConfig.SetAndSaveConfigValue("EnableDiscordIdleStatus", value);
+ SetActivity(_lastActivityStatus); // Refresh activity status to the last one
+ }
+ }
+
+ public bool IsEnabled
+ {
+ get => LauncherConfig.GetAppConfigValue("EnableDiscordRPC");
+ set
+ {
+ bool isPreviouslyEnabled = LauncherConfig.GetAppConfigValue("EnableDiscordRPC");
+ LauncherConfig.SetAndSaveConfigValue("EnableDiscordRPC", value);
+
+ if (value) Start();
+ else Stop();
+
+ // Refresh activity status if it was previously disabled.
+ if (!isPreviouslyEnabled &&
+ value != isPreviouslyEnabled)
+ {
+ SetActivity(_lastActivityStatus);
+ }
+ }
+ }
+
+ private readonly ConcurrentDictionary _cachedStartTimes = [];
+ private readonly ILogger _sharedLogger;
+ private PresetConfig? _currentPresetConfig;
+
+ public DiscordRpcManager()
+ {
+ _sharedLogger = ILoggerHelper.GetILogger("DiscordRPC");
+ PresenceSetChannel = Channel.CreateUnbounded(new UnboundedChannelOptions
+ {
+ SingleWriter = true
+ });
+
+ PresenceSetThread = new Thread(PresenceSetterInvoke)
+ {
+ IsBackground = true
+ };
+ PresenceSetThread.Start();
+
+ // Initialize from start if enabled
+ if (IsEnabled)
+ {
+ Start();
+ }
+ }
+
+ public void Dispose()
+ {
+ if (Interlocked.Exchange(ref IsDisposed, true))
+ return;
+
+ // Stop presence RPC
+ Stop();
+
+ // Complete the writer and dispose the wait handle.
+ PresenceSetChannel.Writer.TryComplete();
+ _isReadyWaitHandle.Dispose();
+ }
+
+ public async void PresenceSetterInvoke(object? ctx)
+ {
+ try
+ {
+ ChannelReader reader = PresenceSetChannel.Reader;
+
+ while (!IsDisposed && await reader.WaitToReadAsync())
+ {
+ while (reader.TryRead(out RichPresence? presence))
+ {
+ if (IsDisposed)
+ {
+ return;
+ }
+
+ // Blocks and wait until the ready signal is set.
+ _isReadyWaitHandle.WaitOne();
+ Client?.SetPresence(presence);
+ }
+ }
+ }
+ catch (ObjectDisposedException)
+ {
+ // ignore
+
+ // From @neon-nyan:
+ // The reason why this is ignored, is because the disposed exception will
+ // come from the _isReadyWaitHandle. The EventWaitHandle doesn't have such
+ // property or field to check whether the handle is already disposed or
+ // not anyway, so we just yeet the exception.
+ }
+ catch (Exception e)
+ {
+ _sharedLogger.LogError(e, "An error has occurred while setting presence on the RPC client.");
+ }
+ }
+
+ public void Stop()
+ {
+ DiscordRpcClient? oldClient = Interlocked.Exchange(ref Client, null);
+ if (oldClient == null)
+ {
+ return;
+ }
+
+ // Reset the channel by flushing all pending presences
+ while (PresenceSetChannel.Reader.TryRead(out _)) { }
+
+ if (!Volatile.Read(ref IsDisposed))
+ {
+ // Reset wait handle and block presence update until the
+ // client is ready or started.
+ _isReadyWaitHandle.Reset();
+ }
+
+ oldClient.OnReady -= EventClientOnReady;
+ oldClient.OnPresenceUpdate -= EventClientOnPresenceUpdate;
+ oldClient.Dispose();
+ }
+
+ public void Start()
+ {
+ // If not enabled, choose to not initialize the client.
+ if (!IsEnabled)
+ {
+ return;
+ }
+
+ ulong presenceId = _lastPresenceId == 0
+ ? LauncherConfig.AppDiscordApplicationID
+ : _lastPresenceId;
+
+ // Initialize new client and replace the field atomically.
+ Interlocked.Exchange(ref Client, new DiscordRpcClient($"{presenceId}", _sharedLogger));
+ Client.OnReady += EventClientOnReady;
+ Client.OnPresenceUpdate += EventClientOnPresenceUpdate;
+ if (!Client.Initialize())
+ {
+ _sharedLogger.LogInformation("Failed while trying to initialize the client!");
+ }
+ }
+
+ private void EventClientOnReady(object sender, ReadyMessage? msg)
+ {
+ _sharedLogger.LogInformation("Connected to Discord with user {username}", msg?.User?.Username);
+ _isReadyWaitHandle.Set(); // Unblock the presence update thread.
+ }
+
+ private void EventClientOnPresenceUpdate(object sender, PresenceMessage? msg)
+ {
+ if (msg?.Presence == null)
+ {
+ _sharedLogger.LogInformation("Activity cleared!");
+ }
+ else
+ {
+ _sharedLogger.LogInformation("Activity updated! => {msg}", msg.Presence.State == null
+ ? msg.Presence.Details
+ : $"{msg.Presence.Details} - {msg.Presence.State}");
+ }
+ }
+
+ public void SetPresence(PresetConfig? config)
+ {
+ Interlocked.Exchange(ref _currentPresetConfig, config);
+ if (config == null)
+ {
+ Interlocked.Exchange(ref _lastPresenceId, 0);
+ return;
+ }
+
+ ulong presenceId = GetDiscordPresenceId(config);
+ Interlocked.Exchange(ref _lastPresenceId, presenceId);
+
+ // We intentionally stop and start the client to refresh / re-create the client
+ // with the new presence ID.
+ Stop();
+ Start();
+ }
+
+ public void SetActivity(DiscordActivityType type = DiscordActivityType.None, DateTime? specifiedStartTime = null)
+ {
+ Interlocked.Exchange(ref _lastActivityStatus, type);
+
+ // Prevent from exhausting the Presence channel if not enabled.
+ if (!IsEnabled) return;
+
+ // Make sure to re-enable the client if it was not initialized while the manager is not disposed yet.
+ if (Volatile.Read(ref Client) == null &&
+ !IsDisposed &&
+ IsEnabled)
+ {
+ Start();
+ }
+
+ LangParamsMisc? langMisc = Locale.Current.Lang?._Misc;
+ RichPresence? presence = type switch
+ {
+ DiscordActivityType.Play => PresenceBuilder.BuildTimedState(IsGameStatusEnabled ? langMisc?.DiscordRP_InGame : langMisc?.DiscordRP_Play, this, specifiedStartTime),
+ DiscordActivityType.Update => PresenceBuilder.BuildTimedState(langMisc?.DiscordRP_Update, this, specifiedStartTime),
+ DiscordActivityType.Idle => IsShowOnIdle ? PresenceBuilder.BuildIdleState(this) : null,
+ DiscordActivityType.Repair => PresenceBuilder.BuildGenericState(langMisc?.DiscordRP_Repair, this),
+ DiscordActivityType.Cache => PresenceBuilder.BuildGenericState(langMisc?.DiscordRP_Cache, this),
+ DiscordActivityType.GameSettings => PresenceBuilder.BuildGenericState(langMisc?.DiscordRP_GameSettings, this),
+ DiscordActivityType.AppSettings => PresenceBuilder.BuildGenericState(langMisc?.DiscordRP_AppSettings, this),
+ _ => IsShowOnIdle ? new RichPresence
+ {
+ Details = Locale.Current.Lang?._Misc?.DiscordRP_Default,
+ Assets = new Assets
+ {
+ LargeImageKey = PresenceBuilder.DefaultLauncherLogo,
+ LargeImageText = PresenceBuilder.DefaultLauncherLogoTooltip
+ },
+ Timestamps = null!
+ } : null
+ };
+
+ PresenceSetChannel.Writer.TryWrite(presence);
+ }
+
+ private static ulong GetDiscordPresenceId(PresetConfig presetConfig)
+ {
+ return presetConfig.GameName switch
+ {
+ "Honkai: Star Rail" => LauncherConfig.AppDiscordApplicationIDHsr,
+ "Honkai Impact 3rd" => LauncherConfig.AppDiscordApplicationIDHi3,
+ "Genshin Impact" => LauncherConfig.AppDiscordApplicationIDGi,
+ "Zenless Zone Zero" => LauncherConfig.AppDiscordApplicationIDZzz,
+ _ => TryGetPresenceFromPlugin(presetConfig)
+ };
+
+ static ulong TryGetPresenceFromPlugin(PresetConfig presetConfig)
+ {
+ if (presetConfig is not PluginPresetConfigWrapper { DiscordPresenceContext: { IsFeatureAvailable: true } discordContext } ||
+ discordContext.PresenceId == 0)
+ {
+ return LauncherConfig.AppDiscordApplicationID; // Default
+ }
+
+ return discordContext.PresenceId;
+ }
+ }
+
+ private static class PresenceBuilder
+ {
+ private const string CollapseLogoExt = "https://collapselauncher.com/img/logo@2x.webp";
+
+ public const string DefaultLauncherLogo = "launcher-logo-new";
+ public static readonly string DefaultLauncherLogoTooltip = $"Collapse Launcher v{LauncherUpdateHelper.LauncherCurrentVersionString} "
+ + $"{(LauncherConfig.IsPreview ? "Preview" : "Stable")}";
+
+ public static RichPresence BuildTimedState(string? activityName, DiscordRpcManager manager, DateTime? specifiedStartTime = null)
+ {
+ bool isGameStatusEnabled = manager.IsGameStatusEnabled;
+ PresetConfig? presetConfig = manager._currentPresetConfig;
+
+ int presetConfigHashId = presetConfig?.HashID ?? 0;
+
+ // Try to get the existing start offset or create a new one if not exist.
+ DateTime startOffset = manager._cachedStartTimes.GetOrAdd(presetConfigHashId, specifiedStartTime ?? DateTime.UtcNow);
+
+ string? currentGameName = presetConfig?.GameName;
+ string? translatedGameName = MetadataHelper.GetTranslatedTitle(currentGameName);
+
+ return BuildGenericState($"{activityName} {(!isGameStatusEnabled ? translatedGameName : null)}",
+ manager,
+ new Timestamps
+ {
+ Start = startOffset
+ });
+ }
+
+ public static RichPresence BuildGenericState(string? activityName, DiscordRpcManager manager, Timestamps? timestamps = null)
+ {
+ PresetConfig? presetConfig = manager._currentPresetConfig;
+ TryGetGameIconsAndTranslatedNames(presetConfig,
+ out string? largeIconUrl,
+ out string? largeIconTooltip,
+ out string? smallIconUrl,
+ out string? smallIconTooltip,
+ out string? translatedGameRegion);
+
+ return new RichPresence
+ {
+ Details = activityName,
+ State = $"{Locale.Current.Lang?._Misc?.DiscordRP_Region} {translatedGameRegion}",
+ Assets = new Assets
+ {
+ LargeImageKey = largeIconUrl,
+ LargeImageText = largeIconTooltip,
+ SmallImageKey = smallIconUrl,
+ SmallImageText = smallIconTooltip
+ },
+ Timestamps = timestamps
+ };
+ }
+
+ public static RichPresence BuildIdleState(DiscordRpcManager manager)
+ {
+ // Try to remove existing cached start time (Reset)
+ PresetConfig? presetConfig = manager._currentPresetConfig;
+ int presetConfigHashId = presetConfig?.GetHashCode() ?? 0;
+ manager._cachedStartTimes.TryRemove(presetConfigHashId, out _);
+ return BuildGenericState(Locale.Current.Lang?._Misc?.DiscordRP_Idle, manager);
+ }
+
+ private static void TryGetGameIconsAndTranslatedNames(
+ PresetConfig? presetConfig,
+ out string? largeIconUrl,
+ out string? largeIconTooltip,
+ out string? smallIconUrl,
+ out string? smallIconTooltip,
+ out string? translatedGameRegion)
+ {
+ Unsafe.SkipInit(out largeIconUrl);
+ Unsafe.SkipInit(out largeIconTooltip);
+ Unsafe.SkipInit(out smallIconUrl);
+ Unsafe.SkipInit(out smallIconTooltip);
+
+ string? currentGameName = presetConfig?.GameName;
+ string? currentGameRegion = presetConfig?.ZoneName;
+ string? translatedGameName = MetadataHelper.GetTranslatedTitle(currentGameName);
+ translatedGameRegion = MetadataHelper.GetTranslatedRegion(currentGameRegion);
+
+ // Try to get icons from plugin if available.
+ TryGetPluginGameIcons(presetConfig,
+ out largeIconUrl,
+ out largeIconTooltip,
+ out smallIconUrl,
+ out smallIconTooltip,
+ out bool isPluginGame);
+
+ largeIconUrl ??= isPluginGame ? CollapseLogoExt : $"game-{presetConfig?.GameType.ToString().ToLower()}-logo";
+ largeIconTooltip ??= $"{translatedGameName} - {translatedGameRegion}";
+ smallIconUrl ??= isPluginGame ? CollapseLogoExt : DefaultLauncherLogo;
+ smallIconTooltip ??= DefaultLauncherLogoTooltip;
+ }
+
+ private static void TryGetPluginGameIcons(PresetConfig? presetConfig,
+ out string? largeIconUrl,
+ out string? largeIconTooltip,
+ out string? smallIconUrl,
+ out string? smallIconTooltip,
+ out bool isPluginGame)
+ {
+ Unsafe.SkipInit(out largeIconUrl);
+ Unsafe.SkipInit(out largeIconTooltip);
+ Unsafe.SkipInit(out smallIconUrl);
+ Unsafe.SkipInit(out smallIconTooltip);
+ Unsafe.SkipInit(out isPluginGame);
+
+ if (presetConfig is not PluginPresetConfigWrapper asPluginPresetConfig)
+ {
+ return;
+ }
+
+ isPluginGame = true;
+ if (!asPluginPresetConfig.DiscordPresenceContext.IsFeatureAvailable)
+ {
+ return;
+ }
+
+ largeIconUrl = asPluginPresetConfig.DiscordPresenceContext.LargeIconUrl;
+ largeIconTooltip = asPluginPresetConfig.DiscordPresenceContext.LargeIconTooltip;
+ smallIconUrl = asPluginPresetConfig.DiscordPresenceContext.SmallIconUrl;
+ smallIconTooltip = asPluginPresetConfig.DiscordPresenceContext.SmallIconTooltip;
+ }
+ }
+}
diff --git a/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs b/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs
index 2ec01077cf..d1d83fc86b 100644
--- a/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs
+++ b/CollapseLauncher/Classes/EventsManagement/BackgroundActivityManager.cs
@@ -259,6 +259,12 @@ private static void AttachEventToNotification(PresetConfig presetConfig, IBackgr
activity!.ProgressChanged += progressChangedEventHandler;
activity!.StatusChanged += statusChangedEventHandler;
+ // The notification can be attached after the operation has already
+ // started. Hydrate it immediately instead of waiting for another
+ // progress/status event and leaving the placeholder text visible.
+ progressChangedEventHandler(activity, activity.Progress);
+ statusChangedEventHandler(activity, activity.Status);
+
activity.FlushingTrigger += (_, _) =>
{
activity.ProgressChanged -= progressChangedEventHandler;
diff --git a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs
index 5fe490be70..2214d2376f 100644
--- a/CollapseLauncher/Classes/Extension/UIElementExtensions.cs
+++ b/CollapseLauncher/Classes/Extension/UIElementExtensions.cs
@@ -57,7 +57,11 @@ internal static T BindNavigationViewItemText(this T element, object? localeOb
internal static T BindTooltipToLocale(this T element, object? localeObjBinding, string localePropertyName, IValueConverter? converter = null, object? converterParameter = null)
where T : DependencyObject
{
- TextBlock tooltipTextBlock = new();
+ TextBlock tooltipTextBlock = new()
+ {
+ TextWrapping = TextWrapping.Wrap,
+ TextTrimming = TextTrimming.CharacterEllipsis
+ };
tooltipTextBlock.BindProperty(TextBlock.TextProperty,
localeObjBinding,
localePropertyName,
@@ -144,14 +148,14 @@ internal static void SetAllControlsCursorRecursive(this UIElement? element, Inpu
}
}
- internal static void BindProperty(this FrameworkElement element,
- object? objectToBind,
- string propertyName,
- DependencyProperty dependencyProperty,
- BindingMode bindingMode,
- IValueConverter? converter = null)
+ internal static void BindProperty(this FrameworkElement? element,
+ object? objectToBind,
+ string propertyName,
+ DependencyProperty dependencyProperty,
+ BindingMode bindingMode,
+ IValueConverter? converter = null)
{
- element.SetBinding(dependencyProperty, new Binding
+ element?.SetBinding(dependencyProperty, new Binding
{
Source = objectToBind,
Mode = bindingMode,
@@ -1306,5 +1310,14 @@ internal static T Create(Action? setAttributeDelegate = null)
return element;
}
+
+ internal static void UpdateLayoutAndBinding(this ComboBox comboBox)
+ {
+ comboBox.UpdateLayout();
+ object? lastSelectedItem = comboBox.SelectedItem;
+ comboBox.SelectedItem = null;
+ comboBox.SelectedItem = lastSelectedItem;
+ comboBox.UpdateLayout();
+ }
}
}
diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs
index e907634086..50b1e78d8a 100644
--- a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs
+++ b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/ImportExportBase.cs
@@ -1,6 +1,9 @@
using CollapseLauncher.Helper;
+using CollapseLauncher.Helper.Database;
+using CollapseLauncher.Helper.Metadata;
using CollapseLauncher.Interfaces;
using Hi3Helper;
+using Hi3Helper.Data;
using Hi3Helper.EncTool;
using Hi3Helper.UABT;
using Hi3Helper.UABT.Binary;
@@ -70,11 +73,11 @@ public RegistryKey? RegistryRoot
return RegistryRoot;
}
- public async Task ImportSettings(string? gameBasePath = null)
+ public async Task ImportSettings(string? gameBasePath = null, string? path = null)
{
try
{
- string path = await FileDialogNative.GetFilePicker(new Dictionary { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegImportTitle);
+ path ??= await FileDialogNative.GetFilePicker(new Dictionary { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegImportTitle);
if (string.IsNullOrEmpty(path)) throw new OperationCanceledException(Locale.Current.Lang?._GameSettingsPage?.SettingsRegErr1);
@@ -190,11 +193,11 @@ private void ReadV3Values(Stream fs, string? gameBasePath)
ImportStreamToFiles(stream, gameBasePath);
}
- public async Task ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null)
+ public async Task ExportSettings(bool isCompressed = true, string? parentPathToImport = null, string[]? relativePathToImport = null, string? path = null)
{
try
{
- string path = await FileDialogNative.GetFileSavePicker(new Dictionary { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegExportTitle);
+ path ??= await FileDialogNative.GetFileSavePicker(new Dictionary { { "Collapse Registry", "*.clreg" } }, Locale.Current.Lang?._GameSettingsPage?.SettingsRegExportTitle);
EnsureFileSaveHasExtension(ref path, ".clreg");
if (string.IsNullOrEmpty(path)) throw new OperationCanceledException(Locale.Current.Lang?._GameSettingsPage?.SettingsRegErr1);
@@ -533,5 +536,70 @@ protected virtual void ReadBinary(EndianBinaryReader reader, string valueName)
_ = reader.Read(val, 0, len);
RegistryRoot?.SetValue(valueName, val, RegistryValueKind.Binary);
}
+
+
+ # region database
+
+ private string GameTypeValue => (GameVersionManager?.GameType is not GameNameType.Plugin
+ ? GameVersionManager?.GameType.ToString() : GameVersionManager?.GameName.Replace(" ", "")) ?? "UNKNOWN";
+ private string KeySettings => $"{GameTypeValue}-{GameVersionManager?.GameRegion}-gs";
+ private string KeyLastUpdated => $"{GameTypeValue}-{GameVersionManager?.GameRegion}-gs-lu";
+
+ public async Task PushToDatabase()
+ {
+ try
+ {
+ string path = Path.GetTempFileName();
+ _ = await ExportSettings(false, null, null, path);
+
+ if (!File.Exists(path)) return null;
+
+ var fi = new FileInfo(path);
+ if (fi.Length == 0) return null;
+ byte[] fileBytes = await File.ReadAllBytesAsync(path);
+ await DbHandler.StoreKeyValue(KeySettings, "", true, true, fileBytes);
+ await DbHandler.StoreKeyValue(KeyLastUpdated, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), true);
+ fi.Delete();
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ return ex;
+ }
+
+ return null;
+ }
+
+ public async Task GetFromDatabase()
+ {
+ try
+ {
+ var retval = await DbHandler.QueryKey(KeySettings, true, true);
+
+ if (retval == null) throw new NullReferenceException();
+
+ string path = Path.GetTempFileName();
+ await File.WriteAllBytesAsync(path, Convert.FromHexString(retval));
+
+ string? gameBasePath = null;
+ if (GameVersionManager?.GameType == GameNameType.Zenless)
+ {
+ gameBasePath = ConverterTool.NormalizePath(GameVersionManager?.GameDirPath);
+ }
+
+ await ImportSettings(gameBasePath, path);
+
+ File.Delete(path);
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine(ex);
+ return ex;
+ }
+
+ return null;
+ }
+
+ #endregion
}
}
diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs
index 2dc0372ada..32e6f1e1e7 100644
--- a/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs
+++ b/CollapseLauncher/Classes/GameManagement/GameSettings/BaseClass/MagicNodeBaseValues.cs
@@ -28,13 +28,6 @@ public enum JsonEnumStoreType
internal static class MagicNodeBaseValuesExt
{
- // ReSharper disable once UnusedMember.Local
- private static readonly JsonSerializerOptions JsonSerializerOpts = new()
- {
- AllowTrailingCommas = true,
- ReadCommentHandling = JsonCommentHandling.Skip
- };
-
private static JsonObject EnsureCreatedObject(this JsonNode? node, string keyName)
{
// If the node is empty, then create a new instance of it
@@ -336,7 +329,11 @@ internal class MagicNodeBaseValues : NotifyPropertyChanged, IGameSettingsValu
private SettingsGameVersionManager GameVersionManager { get; set; }
[JsonIgnore]
- protected JsonNode? SettingsJsonNode { get; private set; }
+ protected JsonNode? SettingsJsonNode
+ {
+ get;
+ private set;
+ }
[JsonIgnore]
public IGameSettings ParentGameSettings => null!;
@@ -406,15 +403,14 @@ public void Save()
{
// Get the file and dir path
string filePath = GameVersionManager.ConfigFilePath;
- string? fileDirPath = Path.GetDirectoryName(filePath);
-
- // Create the dir if not exist
- if (string.IsNullOrEmpty(fileDirPath) && !Directory.Exists(fileDirPath))
- Directory.CreateDirectory(fileDirPath!);
// Write into the file
string jsonString = SettingsJsonNode.SerializeJsonNode(TypeInfo, false, true);
Sleepy.WriteString(filePath, jsonString, Magic);
+
+#if DEBUG
+ Logger.LogWriteLine($"Serialized data:\r\n{jsonString}", LogType.Debug, true);
+#endif
}
public override bool Equals(object? obj)
diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/FileClass/GeneralData.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/FileClass/GeneralData.cs
index 67a9075499..a424ba10f6 100644
--- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/FileClass/GeneralData.cs
+++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/FileClass/GeneralData.cs
@@ -3,7 +3,6 @@
using CollapseLauncher.GameSettings.Zenless.JsonProperties;
using Hi3Helper;
using System;
-using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
@@ -20,34 +19,10 @@
namespace CollapseLauncher.GameSettings.Zenless
{
[GeneratedBindableCustomProperty]
- internal sealed partial class GeneralData : MagicNodeBaseValues, IDisposable
+ internal sealed partial class GeneralData : MagicNodeBaseValues
{
- #region Disposer
-
- ~GeneralData()
- {
- _systemSettingDataMap = null;
- _keyboardBindingMap = null;
- _mouseBindingMap = null;
- _gamepadBindingMap = null;
-
- GC.Collect();
- }
-
- public void Dispose()
- {
- GC.SuppressFinalize(this);
- }
-
- #endregion
-
#region Node Based Properties
- private JsonNode? _systemSettingDataMap;
- private JsonNode? _keyboardBindingMap;
- private JsonNode? _mouseBindingMap;
- private JsonNode? _gamepadBindingMap;
-
[JsonPropertyName("SystemSettingDataMap")]
[JsonIgnore] // We ignore this one from getting serialized to default JSON value
public JsonNode SystemSettingDataMap
@@ -55,8 +30,8 @@ public JsonNode SystemSettingDataMap
// Cache the SystemSettingDataMap inside the parent SettingsJsonNode
// and ensure that the node for SystemSettingDataMap exists. If not exist,
// create a new one (via GetAsJsonNode()).
- get => _systemSettingDataMap ??= SettingsJsonNode.GetAsJsonNode("SystemSettingDataMap");
- set => _systemSettingDataMap?.SetAsJsonNode("SystemSettingDataMap", value);
+ get => field ??= SettingsJsonNode.GetAsJsonNode("SystemSettingDataMap");
+ set => field?.SetAsJsonNode("SystemSettingDataMap", value);
}
[JsonPropertyName("KeyboardBindingMap")]
@@ -66,8 +41,8 @@ public JsonNode KeyboardBindingMap
// Cache the KeyboardBindingMap inside the parent SettingsJsonNode
// and ensure that the node for KeyboardBindingMap exists. If not exist,
// create a new one (via GetAsJsonNode()).
- get => _keyboardBindingMap ??= SettingsJsonNode.GetAsJsonNode("KeyboardBindingMap");
- set => _keyboardBindingMap?.SetAsJsonNode("KeyboardBindingMap", value);
+ get => field ??= SettingsJsonNode.GetAsJsonNode("KeyboardBindingMap");
+ set => field?.SetAsJsonNode("KeyboardBindingMap", value);
}
[JsonPropertyName("MouseBindingMap")]
@@ -77,8 +52,8 @@ public JsonNode MouseBindingMap
// Cache the MouseBindingMap inside the parent SettingsJsonNode
// and ensure that the node for MouseBindingMap exists. If not exist,
// create a new one (via GetAsJsonNode()).
- get => _mouseBindingMap ??= SettingsJsonNode.GetAsJsonNode("MouseBindingMap");
- set => _mouseBindingMap?.SetAsJsonNode("MouseBindingMap", value);
+ get => field ??= SettingsJsonNode.GetAsJsonNode("MouseBindingMap");
+ set => field?.SetAsJsonNode("MouseBindingMap", value);
}
[JsonPropertyName("GamepadBindingMap")]
@@ -88,26 +63,23 @@ public JsonNode GamepadBindingMap
// Cache the GamepadBindingMap inside the parent SettingsJsonNode
// and ensure that the node for GamepadBindingMap exists. If not exist,
// create a new one (via GetAsJsonNode()).
- get => _gamepadBindingMap ??= SettingsJsonNode.GetAsJsonNode("GamepadBindingMap");
- set => _gamepadBindingMap?.SetAsJsonNode("GamepadBindingMap", value);
+ get => field ??= SettingsJsonNode.GetAsJsonNode("GamepadBindingMap");
+ set => field?.SetAsJsonNode("GamepadBindingMap", value);
}
[JsonPropertyName("PlayerPrefs_StringContainer")]
- [JsonIgnore]
- [field: AllowNull, MaybeNull] // We ignore this one from getting serialized to default JSON value
+ [JsonIgnore] // We ignore this one from getting serialized to default JSON value
public JsonNode PlayerPrefsStringContainer
{
// Cache the PlayerPrefsStringContainer inside the parent SettingsJsonNode
// and ensure that the node for PlayerPrefsStringContainer exists. If not exist,
// create a new one (via GetAsJsonNode()).
- get => field ??=
- SettingsJsonNode.GetAsJsonNode("PlayerPrefs_StringContainer");
+ get => field ??= SettingsJsonNode.GetAsJsonNode("PlayerPrefs_StringContainer");
set => field?.SetAsJsonNode("PlayerPrefs_StringContainer", value);
}
[JsonPropertyName("PlayerPrefs_IntContainer")]
- [JsonIgnore]
- [field: AllowNull, MaybeNull] // We ignore this one from getting serialized to default JSON value
+ [JsonIgnore] // We ignore this one from getting serialized to default JSON value
public JsonNode PlayerPrefsIntContainer
{
// Cache the PlayerPrefsIntContainer inside the parent SettingsJsonNode
@@ -118,15 +90,13 @@ public JsonNode PlayerPrefsIntContainer
}
[JsonPropertyName("PlayerPrefs_FloatContainer")]
- [JsonIgnore]
- [field: AllowNull, MaybeNull] // We ignore this one from getting serialized to default JSON value
+ [JsonIgnore] // We ignore this one from getting serialized to default JSON value
public JsonNode PlayerPrefsFloatContainer
{
// Cache the PlayerPrefsFloatContainer inside the parent SettingsJsonNode
// and ensure that the node for PlayerPrefsFloatContainer exists. If not exist,
// create a new one (via GetAsJsonNode()).
- get => field ??=
- SettingsJsonNode.GetAsJsonNode("PlayerPrefs_FloatContainer");
+ get => field ??= SettingsJsonNode.GetAsJsonNode("PlayerPrefs_FloatContainer");
set => field?.SetAsJsonNode("PlayerPrefs_FloatContainer", value);
}
@@ -617,22 +587,22 @@ public static GeneralData Load()
public new static GeneralData LoadWithMagic(byte[] magic, SettingsGameVersionManager versionManager,
JsonTypeInfo typeInfo)
{
- var returnVal = MagicNodeBaseValues.LoadWithMagic(magic, versionManager, typeInfo);
+ GeneralData returnVal = MagicNodeBaseValues.LoadWithMagic(magic, versionManager, typeInfo);
#if DEBUG
- const bool isPrintDebug = true;
- if (isPrintDebug)
- {
- Logger.LogWriteLine($"Zenless GeneralData parsed value:\r\n\t" +
- $"FPS : {returnVal.Fps}\r\n\t" +
- $"VSync : {returnVal.VSync}\r\n\t" +
- $"RenRes: {returnVal.RenderResolution}\r\n\t" +
- $"AA : {returnVal.AntiAliasing}\r\n\t" +
- $"Shadow: {returnVal.ShadowQuality}\r\n\t" +
- $"CharQ : {returnVal.CharacterQuality}\r\n\t" +
- $"RelfQ : {returnVal.ReflectionQuality}\r\n\t",
- LogType.Debug, true);
- }
+ const bool isPrintDebug = true;
+ if (isPrintDebug)
+ {
+ Logger.LogWriteLine($"Zenless GeneralData parsed value:\r\n\t" +
+ $"FPS : {returnVal.Fps}\r\n\t" +
+ $"VSync : {returnVal.VSync}\r\n\t" +
+ $"RenRes: {returnVal.RenderResolution}\r\n\t" +
+ $"AA : {returnVal.AntiAliasing}\r\n\t" +
+ $"Shadow: {returnVal.ShadowQuality}\r\n\t" +
+ $"CharQ : {returnVal.CharacterQuality}\r\n\t" +
+ $"RelfQ : {returnVal.ReflectionQuality}\r\n\t",
+ LogType.Debug, true);
+ }
#endif
return returnVal;
diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/JsonProperties/Properties.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/JsonProperties/Properties.cs
index 1c9ff0bf2f..79379a3d0f 100644
--- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/JsonProperties/Properties.cs
+++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/JsonProperties/Properties.cs
@@ -28,7 +28,7 @@ public readonly struct SystemSettingLocalData
public void SetDataEnum(TDataEnum value, JsonEnumStoreType enumStoreType = JsonEnumStoreType.AsNumber)
where TDataEnum : struct, Enum => _node.SetNodeValueEnum("Data", value, enumStoreType);
- public SystemSettingLocalData([NotNull] JsonNode node, TData defaultData = default, int defaultVersion = 1)
+ public SystemSettingLocalData([NotNull] JsonNode node, TData defaultData = default, int defaultVersion = 0)
{
ArgumentNullException.ThrowIfNull(node);
_node = node;
@@ -44,7 +44,7 @@ public SystemSettingLocalData([NotNull] JsonNode node, TData defaultData = defau
public static class SystemSettingLocalDataExt
{
public static SystemSettingLocalData AsSystemSettingLocalData(
- [NotNull] this JsonNode? node, string keyName, TData defaultData = default, int defaultVersion = 1)
+ [NotNull] this JsonNode? node, string keyName, TData defaultData = default, int defaultVersion = 0)
where TData : struct
{
ArgumentNullException.ThrowIfNull(node);
diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Settings.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Settings.cs
index 9718db6c2e..edebfc692c 100644
--- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Settings.cs
+++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Settings.cs
@@ -39,7 +39,12 @@ private byte[] MagicReDo
#endregion
#region Properties
- public GeneralData GeneralData { get; private set; }
+
+ public GeneralData GeneralData
+ {
+ get;
+ private set;
+ }
#endregion
public ZenlessSettings(IGameVersion gameVersionManager) : base(gameVersionManager)
@@ -57,7 +62,6 @@ public sealed override void InitializeSettings()
base.InitializeSettings();
SettingsScreen = ScreenManager.Load(this);
- GeneralData?.Dispose();
GeneralData = GeneralData.LoadWithMagic(
MagicReDo,
SettingsGameVersionManager.Create(GameVersionManager, ZZZSettingsConfigFile, "GENERAL_DATA.bin"),
diff --git a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Sleepy.cs b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Sleepy.cs
index 87c4309354..07e050c15c 100644
--- a/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Sleepy.cs
+++ b/CollapseLauncher/Classes/GameManagement/GameSettings/Zenless/Sleepy.cs
@@ -1,6 +1,3 @@
-// ReSharper disable CommentTypo
-// ReSharper disable UnusedMember.Local
-// ReSharper disable UnusedVariable
/*
* Initial Implementation Credit by: @Shatyuka
*/
@@ -10,16 +7,19 @@
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Text;
+// ReSharper disable RedundantUnsafeContext
+// ReSharper disable UnusedMember.Local
// ReSharper disable IdentifierTypo
namespace CollapseLauncher.GameSettings.Zenless;
#nullable enable
-internal static class Sleepy
+internal static unsafe class Sleepy
{
// https://github.com/dotnet/runtime/blob/a7efcd9ca9255dc9faa8b4a2761cdfdb62619610/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryEnums.cs#L7C1-L32C6
- private enum BinaryHeaderEnum
+ private enum BinaryHeaderEnum : byte
{
SerializedStreamHeader = 0,
Object = 1,
@@ -44,47 +44,12 @@ private enum BinaryHeaderEnum
CrossAppDomainAssembly = 20,
MethodCall = 21,
MethodReturn = 22,
- BinaryReference = -1
- }
-
- // https://github.com/dotnet/runtime/blob/a7efcd9ca9255dc9faa8b4a2761cdfdb62619610/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryEnums.cs#L35
- private enum BinaryTypeEnum
- {
- Primitive = 0,
- String = 1,
- Object = 2,
- ObjectUrt = 3,
- ObjectUser = 4,
- ObjectArray = 5,
- StringArray = 6,
- PrimitiveArray = 7
- }
-
- // https://github.com/dotnet/runtime/blob/a7efcd9ca9255dc9faa8b4a2761cdfdb62619610/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryEnums.cs#L47
- private enum BinaryArrayTypeEnum
- {
- Single = 0,
- Jagged = 1,
- Rectangular = 2,
- SingleOffset = 3,
- JaggedOffset = 4,
- RectangularOffset = 5
- }
-
- // https://github.com/dotnet/runtime/blob/a7efcd9ca9255dc9faa8b4a2761cdfdb62619610/src/libraries/System.Runtime.Serialization.Formatters/src/System/Runtime/Serialization/Formatters/Binary/BinaryEnums.cs#L99
- private enum InternalArrayTypeE
- {
- Empty = 0,
- Single = 1,
- Jagged = 2,
- Rectangular = 3,
- Base64 = 4
}
internal static string ReadString(string filePath, ReadOnlySpan magic)
{
// Get the FileInfo
- FileInfo fileInfo = new FileInfo(filePath).EnsureNoReadOnly(out bool isExist);
+ FileInfo fileInfo = new FileInfo(filePath).StripAlternateDataStream().EnsureNoReadOnly(out bool isExist);
if (!isExist)
throw new FileNotFoundException("[Sleepy::ReadString] File does not exist!");
@@ -93,76 +58,53 @@ internal static string ReadString(string filePath, ReadOnlySpan magic)
return ReadString(stream, magic);
}
- internal static unsafe string ReadString(Stream stream, ReadOnlySpan magic)
+ [SkipLocalsInit]
+ internal static string ReadString(Stream stream, ReadOnlySpan magic)
{
// Stream assertion
if (!stream.CanRead) throw new ArgumentException("[Sleepy::ReadString] Stream must be readable!", nameof(stream));
// Assign the reader
- using BinaryReader reader = new BinaryReader(stream, Encoding.UTF8, true);
+ using BinaryReader reader = new(stream, Encoding.UTF8, true);
- // Emulate and Assert the BinaryFormatter header info
- reader.EmulateSleepyBinaryFormatterHeaderAssertion();
+ // Emulate and Assert the BinaryFormatter header
+ reader.EmulateReadAssert();
// Get the data length
- int length = reader.GetBinaryFormatterDataLength();
- int magicLength = magic.Length;
+ int length = reader.Read7BitEncodedInt();
// Alloc temporary buffers
- char[] bufferChars = ArrayPool.Shared.Rent(length);
+ Span evil = stackalloc bool[magic.Length];
+ byte[] evilBuffer = ArrayPool.Shared.Rent(length);
+ char[] unevilBuffer = ArrayPool.Shared.Rent(length);
- // Do the do
- CreateEvil(magic, out bool[] evil, out int evilsCount);
- fixed (bool* evp = &evil[0])
- fixed (char* bp = &bufferChars[0])
- {
- try
- {
- // Do the do (pt. 2)
- int j = InternalDecode(magic, evp, reader, length, magicLength, bp);
-
- // Emulate and Assert the BinaryFormatter footer
- reader.EmulateSleepyBinaryFormatterFooterAssertion();
-
- // Return
- return new string(bp, 0, j);
- }
- finally
- {
- // Return and clear the buffer, to only returns the return string.
- ArrayPool.Shared.Return(bufferChars, true);
- }
- }
- }
+ // Read evil data to evil buffer >:)
+ reader.BaseStream.ReadExactly(evilBuffer, 0, length);
- private static unsafe int InternalDecode(ReadOnlySpan magic, bool* evil, BinaryReader reader, int length, int magicLength, char* bp)
- {
- bool eepy = false;
+ try
+ {
+ // Do the do
+ CreateEvil(magic, evil);
- int j = 0;
- int i = 0;
+ // Do the do (pt. 2)
+ int j = InternalRead(magic,
+ evil,
+ evilBuffer.AsSpan(0, length),
+ unevilBuffer.AsSpan(0, length));
- amimir:
- var n = i % magicLength;
- byte c = reader.ReadByte();
- byte ch = (byte)(c ^ magic[n]);
+ // Emulate and Assert the BinaryFormatter footer
+ reader.EmulateReadAssertMessageEnd();
- if (*(evil + n))
- {
- eepy = ch != 0;
+ // Return
+ return new string(unevilBuffer, 0, j);
}
- else
+ finally
{
- if (eepy)
- {
- ch += 0x40;
- eepy = false;
- }
- *(bp + j++) = (char)ch;
+ // Return and clear the buffer, to only returns the return string.
+ evil.Clear();
+ ArrayPool.Shared.Return(evilBuffer, true);
+ ArrayPool.Shared.Return(unevilBuffer, true);
}
-
- if (++i < length) goto amimir;
- return j;
}
internal static void WriteString(string filePath, ReadOnlySpan content, ReadOnlySpan magic)
@@ -182,7 +124,8 @@ internal static void WriteString(string filePath, ReadOnlySpan content, Re
WriteString(stream, content, magic);
}
- internal static unsafe void WriteString(Stream stream, ReadOnlySpan content, ReadOnlySpan magic)
+ [SkipLocalsInit]
+ internal static void WriteString(Stream stream, ReadOnlySpan content, ReadOnlySpan magic)
{
// Stream assertion
if (!stream.CanWrite) throw new ArgumentException("[Sleepy::WriteString] Stream must be writable!", nameof(stream));
@@ -191,178 +134,206 @@ internal static unsafe void WriteString(Stream stream, ReadOnlySpan conten
if (magic.Length == 0) throw new ArgumentException("[Sleepy::WriteString] Magic cannot be empty!", nameof(magic));
// Assign the writer
- using BinaryWriter writer = new BinaryWriter(stream, Encoding.UTF8, true);
+ using BinaryWriter writer = new(stream, Encoding.UTF8, true);
// Emulate to write the BinaryFormatter header
- writer.EmulateSleepyBinaryFormatterHeaderWrite();
+ writer.EmulateWrite();
// Do the do
- int contentLen = content.Length;
- int bufferLen = contentLen * 2;
+ int contentLen = content.Length;
+ int bufferLen = Encoding.UTF8.GetMaxByteCount(contentLen);
// Alloc temporary buffers
- byte[] contentBytes = ArrayPool.Shared.Rent(bufferLen);
- byte[] encodedBytes = ArrayPool.Shared.Rent(bufferLen);
+ Span evil = stackalloc bool[magic.Length];
+ byte[] evilBuffer = ArrayPool.Shared.Rent(bufferLen);
+ byte[] unevilBuffer = ArrayPool.Shared.Rent(bufferLen);
- // Do the do
- CreateEvil(magic, out bool[] evil, out int evilsCount);
-
- fixed (char* cp = &content[0])
- fixed (byte* bp = &contentBytes[0])
- fixed (byte* ep = &encodedBytes[0])
- fixed (bool* evp = &evil[0])
- {
- try
- {
- // Get the string bytes
- _ = Encoding.UTF8.GetBytes(cp, contentLen, bp, bufferLen);
-
- // Do the do (pt. 2)
- int h = InternalWrite(magic, contentLen, bp, ep, evp);
-
- writer.Write7BitEncodedInt(h);
- writer.BaseStream.Write(encodedBytes, 0, h);
- writer.EmulateSleepyBinaryFormatterFooterWrite();
- }
- finally
- {
- // Return and clear the buffer.
- ArrayPool.Shared.Return(contentBytes, true);
- ArrayPool.Shared.Return(encodedBytes, true);
- }
- }
+ try
+ {
+ // Encode content to unevil UTF-8 buffer
+ int unevilBufferLen = Encoding.UTF8.GetBytes(content, unevilBuffer);
+
+ // Do the do
+ CreateEvil(magic, evil);
+
+ // Do the do (pt. 2)
+ int h = InternalWrite(magic,
+ evil,
+ evilBuffer,
+ unevilBuffer.AsSpan(0, unevilBufferLen));
+
+ writer.Write7BitEncodedInt(h);
+ writer.BaseStream.Write(evilBuffer, 0, h);
+ writer.EmulateWriteMessageEnd();
+ }
+ finally
+ {
+ // Return and clear the buffer.
+ evil.Clear();
+ ArrayPool.Shared.Return(evilBuffer, true);
+ ArrayPool.Shared.Return(unevilBuffer, true);
+ }
+ }
+
+ private static int InternalRead(
+ ReadOnlySpan magic,
+ scoped ReadOnlySpan evil,
+ ReadOnlySpan evilBuffer,
+ Span unevilBuffer)
+ {
+ bool eepy = false;
+
+ int j = 0;
+ int i = 0;
+
+ amimir:
+ int n = i % magic.Length;
+ byte c = evilBuffer[i];
+ byte ch = (byte)(c ^ magic[n]);
+
+ if (evil[n])
+ {
+ eepy = ch != 0;
+ }
+ else
+ {
+ if (eepy)
+ {
+ ch += 0x40;
+ eepy = false;
+ }
+ unevilBuffer[j++] = (char)ch;
+ }
+
+ if (++i < evilBuffer.Length) goto amimir;
+ return j;
}
- private static unsafe int InternalWrite(ReadOnlySpan magic, int contentLen, byte* bp, byte* ep, bool* evil)
+ private static int InternalWrite(
+ ReadOnlySpan magic,
+ scoped ReadOnlySpan evil,
+ Span evilBuffer,
+ ReadOnlySpan unevilBuffer)
{
int h = 0;
int i = 0;
int j = 0;
- amimir:
+ amimir:
int n = i % magic.Length;
- byte ch = *(bp + j);
- if (*(evil + n))
+ byte ch = unevilBuffer[j];
+ if (evil[n])
{
byte eepy = 0;
- if (*(bp + j) >= 0x40)
+ if (unevilBuffer[j] >= 0x40)
{
ch -= 0x40;
eepy = 1;
}
- *(ep + h++) = (byte)(eepy ^ magic[n]);
+ evilBuffer[h++] = (byte)(eepy ^ magic[n]);
n = ++i % magic.Length;
}
- *(ep + h++) = (byte)(ch ^ magic[n]);
+ evilBuffer[h++] = (byte)(ch ^ magic[n]);
++i;
++j;
- if (j < contentLen) goto amimir;
+ if (j < unevilBuffer.Length) goto amimir;
return h;
}
- private static void CreateEvil(ReadOnlySpan magic, out bool[] evilist, out int evilsCount)
+ private static void CreateEvil(ReadOnlySpan magic, scoped Span evilist)
{
int magicLength = magic.Length;
int i = 0;
- evilist = new bool[magicLength];
- evilsCount = 0;
- evilist:
+
+ evilist:
int n = i % magicLength;
evilist[i] = (magic[n] & 0xC0) == 0xC0;
- if (evilist[i]) ++evilsCount;
+
if (++i < magicLength) goto evilist;
}
- private static void EmulateSleepyBinaryFormatterHeaderAssertion(this BinaryReader reader)
+ extension(BinaryReader reader)
{
- // Do assert [class] -> [string object]
- // START!
- // Check if the first byte is SerializedStreamHeader
- reader.LogAssertInfoByteEnum(BinaryHeaderEnum.SerializedStreamHeader);
+ private void EmulateReadAssert()
+ {
+ // Check if the record type is a SerializedStreamHeader
+ reader.ReadAssert(BinaryHeaderEnum.SerializedStreamHeader);
- // Check if the type is an Object
- reader.LogAssertInfoInt32Enum(BinaryHeaderEnum.Object);
+ // Check if Root object ID == 1
+ reader.ReadAssert(1);
- // Check if the type is a BinaryReference
- reader.LogAssertInfoInt32Enum(BinaryHeaderEnum.BinaryReference);
+ // Check if No header object is required
+ reader.ReadAssert(-1);
- // Check if the BinaryReference type is a String
- reader.LogAssertInfoInt32Enum(BinaryTypeEnum.String);
+ // Check if the major version is 1
+ reader.ReadAssert(1);
- // Check for the binary array type and check if it's Single
- reader.LogAssertInfoInt32Enum(BinaryArrayTypeEnum.Single);
+ // Check if the minor version is 0
+ reader.ReadAssert(0);
- // Check for the binary type and check if it's StringArray (UTF-8)
- reader.LogAssertInfoByteEnum(BinaryTypeEnum.StringArray);
+ // Check if the record type is an ObjectString
+ reader.ReadAssert(BinaryHeaderEnum.ObjectString);
- // Check for the internal array type and check if it's Single
- reader.LogAssertInfoInt32Enum(InternalArrayTypeE.Single);
- }
+ // Check if Root object ID == 1
+ reader.ReadAssert(1);
+ }
- // Do assert [class] -> [EOF mark]
- // START!
- private static void EmulateSleepyBinaryFormatterFooterAssertion(this BinaryReader reader) =>
- reader.LogAssertInfoByteEnum(BinaryHeaderEnum.MessageEnd);
+ private void EmulateReadAssertMessageEnd() =>
+ reader.ReadAssert(BinaryHeaderEnum.MessageEnd);
- private static void EmulateSleepyBinaryFormatterHeaderWrite(this BinaryWriter writer)
- {
- // Emulate to write Sleepy BinaryFormatter header information
- writer.WriteEnumAsByte(BinaryHeaderEnum.SerializedStreamHeader);
- writer.WriteEnumAsInt32(BinaryHeaderEnum.Object);
- writer.WriteEnumAsInt32(BinaryHeaderEnum.BinaryReference);
- writer.WriteEnumAsInt32(BinaryTypeEnum.String);
- writer.WriteEnumAsInt32(BinaryArrayTypeEnum.Single);
- writer.WriteEnumAsByte(BinaryTypeEnum.StringArray);
- writer.WriteEnumAsInt32(InternalArrayTypeE.Single);
- }
+ [SkipLocalsInit]
+ private void ReadAssert(T assertWith)
+ where T : unmanaged
+ {
+ Span buffer = stackalloc byte[sizeof(T)];
+ _ = reader.BaseStream.Read(buffer);
- // Emulate to write Sleepy BinaryFormatter footer EOF
- private static void EmulateSleepyBinaryFormatterFooterWrite(this BinaryWriter writer) =>
- writer.WriteEnumAsByte(BinaryHeaderEnum.MessageEnd);
+ ref T thisEnum = ref MemoryMarshal.AsRef(buffer);
+ if (IsEqual(ref thisEnum, ref assertWith))
+ return;
- private static void WriteEnumAsByte(this BinaryWriter writer, T headerEnum)
- where T : struct, Enum
- {
- int enumValue = Unsafe.As(ref headerEnum);
- writer.Write((byte)enumValue);
+ throw new InvalidDataException($"[Sleepy::LogAssertInfo] BinaryFormatter header is not valid at stream pos: {reader.BaseStream.Position:x8}. Expecting value: {assertWith} but getting: {thisEnum} instead!");
+ }
}
- private static void WriteEnumAsInt32(this BinaryWriter writer, T headerEnum)
- where T : struct, Enum
- {
- int enumValue = Unsafe.As(ref headerEnum);
- writer.Write(enumValue);
- }
- private static void LogAssertInfoByteEnum(this BinaryReader stream, T assertHeaderEnum)
- where T : struct, Enum
+ extension(BinaryWriter writer)
{
- int currentInt = stream.ReadByte();
- LogAssertInfo(stream, ref assertHeaderEnum, ref currentInt);
- }
+ private void EmulateWrite()
+ {
+ // Emulate to write Sleepy BinaryFormatter header information
+ writer.Write(BinaryHeaderEnum.SerializedStreamHeader);
+ writer.Write(1);
+ writer.Write(-1);
+ writer.Write(1);
+ writer.Write(0);
+ writer.Write(BinaryHeaderEnum.ObjectString);
+ writer.Write(1);
+ }
- private static void LogAssertInfoInt32Enum(this BinaryReader stream, T assertHeaderEnum)
- where T : struct, Enum
- {
- int currentInt = stream.ReadInt32();
- LogAssertInfo(stream, ref assertHeaderEnum, ref currentInt);
- }
+ // Emulate to write Sleepy BinaryFormatter footer EOF
+ private void EmulateWriteMessageEnd() =>
+ writer.Write(BinaryHeaderEnum.MessageEnd);
- private static void LogAssertInfo(BinaryReader reader, ref T assertHeaderEnum, ref int currentInt)
- where T : struct, Enum
- {
- int intAssertCasted = Unsafe.As(ref assertHeaderEnum);
- if (intAssertCasted != currentInt)
+ private void Write(T value)
+ where T : unmanaged
{
- string? assertHeaderEnumValueName = Enum.GetName(assertHeaderEnum);
- T comparedEnumCasted = Unsafe.As(ref currentInt);
- string? comparedHeaderEnumValueName = Enum.GetName(comparedEnumCasted);
-
- throw new InvalidDataException($"[Sleepy::LogAssertInfo] BinaryFormatter header is not valid at stream pos: {reader.BaseStream.Position:x8}. Expecting object enum: {assertHeaderEnumValueName} but getting: {comparedHeaderEnumValueName} instead!");
+ ReadOnlySpan buffer = MemoryMarshal.AsBytes(new ReadOnlySpan(ref value));
+ writer.BaseStream.Write(buffer);
}
}
- private static int GetBinaryFormatterDataLength(this BinaryReader reader) => reader.Read7BitEncodedInt();
+ private static bool IsEqual(ref T from, ref T to)
+ where T : unmanaged
+ => sizeof(T) switch
+ {
+ 1 => Unsafe.As(ref from) == Unsafe.As(ref to),
+ 2 => Unsafe.As(ref from) == Unsafe.As(ref to),
+ 4 => Unsafe.As(ref from) == Unsafe.As(ref to),
+ 8 => Unsafe.As(ref from) == Unsafe.As(ref to),
+ 16 => Unsafe.As(ref from) == Unsafe.As(ref to),
+ _ => MemoryMarshal.AsBytes(new Span(ref from)).SequenceEqual(MemoryMarshal.AsBytes(new Span(ref to)))
+ };
}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ClipboardUtility.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ClipboardUtility.cs
index 40a7ac5db1..d42914b0d8 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ClipboardUtility.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ClipboardUtility.cs
@@ -1,5 +1,4 @@
-using CollapseLauncher.Extension;
-using CollapseLauncher.XAMLs.Theme.CustomControls;
+using CollapseLauncher.XAMLs.Theme.CustomControls;
using Hi3Helper;
using Hi3Helper.Win32.ManagedTools;
using Hi3Helper.Win32.Native.Enums;
@@ -8,13 +7,11 @@
using Hi3Helper.Win32.WinRT.WindowsStream;
using Microsoft.Extensions.Logging;
using Microsoft.Graphics.Canvas;
-using Microsoft.UI.Dispatching;
using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.WindowsRuntime;
-using System.Threading;
using System.Threading.Tasks;
using Windows.Graphics.DirectX;
using Windows.Graphics.DirectX.Direct3D11;
@@ -30,10 +27,8 @@ public static class ClipboardUtility
{
public static async void CopyCurrentFrameToClipboard(
ImageBackgroundManager? instance,
- FrameToCopyType frameToCopyType)
+ FrameToCopyType frameToCopyType)
{
- bool isBackgroundVideo = false;
-
try
{
if (instance == null)
@@ -43,67 +38,14 @@ public static async void CopyCurrentFrameToClipboard(
bool hasBackground = frameToCopyType.HasFlag(FrameToCopyType.Background);
bool hasForeground = frameToCopyType.HasFlag(FrameToCopyType.Foreground);
- bool hasBoth = frameToCopyType == FrameToCopyType.Both;
// Try to get video frame buffer first (if the background is video).
LayeredBackgroundImage? currentElement = instance.CurrentBackgroundElement;
- bool isStaticBackgroundShowing = currentElement is { BackgroundStaticSource: not null, IsVideoPlay: false };
-
- string? usedBackgroundPath = isStaticBackgroundShowing
- ? instance.CurrentSelectedBackgroundContext?
- .OriginBackgroundStaticImagePath
- : instance.CurrentSelectedBackgroundContext?
- .OriginBackgroundImagePath;
-
- CanvasRenderTarget? backgroundCanvas = hasBackground
- ? isStaticBackgroundShowing ? null : currentElement?.LockCanvasRenderTarget()
- : null;
-
- ICanvasResourceCreator? canvasDevice = backgroundCanvas;
- canvasDevice ??= CanvasDevice.GetSharedDevice();
-
- if (backgroundCanvas != null)
- {
- // Copy bitmap to avoid arbitrary write to video frame buffer.
- CanvasRenderTarget oldCanvas = backgroundCanvas;
- backgroundCanvas = new CanvasRenderTarget(canvasDevice, backgroundCanvas.Size._width, backgroundCanvas.Size._height, 96f);
- backgroundCanvas.CopyPixelsFromBitmap(oldCanvas);
-
- isBackgroundVideo = true; // Mark as video so we can unlock the frame later.
- }
-
- // If it's still null (which most likely an image), get the canvas.
- backgroundCanvas ??= hasBackground
- ? await GetImageCanvasTargetBuffer(canvasDevice,
- usedBackgroundPath)
- : await GetImageCanvasTargetBuffer(canvasDevice,
- instance.CurrentSelectedBackgroundContext?
- .OriginOverlayImagePath);
-
- // If background canvas is still null, abort.
- if (backgroundCanvas == null)
- {
+ if (currentElement == null)
return;
- }
-
- // Get foreground canvas if available.
- CanvasRenderTarget? foregroundCanvas = hasBoth && hasForeground
- ? await GetImageCanvasTargetBuffer(canvasDevice,
- instance.CurrentSelectedBackgroundContext?
- .OriginOverlayImagePath)
- : null;
-
- int widthOfForeground = (int)(foregroundCanvas?.SizeInPixels.Width ?? 0u);
- int heightOfForeground = (int)(foregroundCanvas?.SizeInPixels.Height ?? 0u);
- if (foregroundCanvas != null &&
- (backgroundCanvas.SizeInPixels.Width != widthOfForeground ||
- backgroundCanvas.SizeInPixels.Height != heightOfForeground))
- {
- backgroundCanvas = ResizeCanvasToSizeOf(backgroundCanvas, widthOfForeground, heightOfForeground);
- }
- MergeCanvasTargetWith(backgroundCanvas, foregroundCanvas); // Merge overlay (foreground) to background.
- await CopyCanvasTargetBufferToClipboard(backgroundCanvas); // Copy to clipboard
+ using CanvasRenderTarget? sourceFrameCanvas = await currentElement.CopyCurrentFrame(hasBackground, hasForeground);
+ await CopyCanvasTargetBufferToClipboard(sourceFrameCanvas); // Copy to clipboard
}
catch (Exception ex)
{
@@ -111,89 +53,6 @@ public static async void CopyCurrentFrameToClipboard(
LogType.Error,
true);
}
- finally
- {
- if (isBackgroundVideo)
- {
- instance?.CurrentBackgroundElement?.UnlockCanvasRenderTarget();
- }
- }
- }
-
- private static CanvasRenderTarget ResizeCanvasToSizeOf(CanvasRenderTarget canvasToResize, int width, int height)
- {
- CanvasRenderTarget resizedCanvas = new(canvasToResize,
- width,
- height,
- canvasToResize.Dpi);
- using CanvasDrawingSession ds = resizedCanvas.CreateDrawingSession();
- ds.DrawImage(canvasToResize,
- new Windows.Foundation.Rect(0, 0, width, height),
- new Windows.Foundation.Rect(0, 0,
- canvasToResize.SizeInPixels.Width,
- canvasToResize.SizeInPixels.Height),
- 1f,
- CanvasImageInterpolation.HighQualityCubic);
-
- canvasToResize.Dispose();
- return resizedCanvas;
- }
-
- private static async Task GetImageCanvasTargetBuffer(ICanvasResourceCreator canvasDevice, string? path)
- {
- if (string.IsNullOrEmpty(path))
- {
- return null; // Ignore if path is null or empty.
- }
-
- // Get source image as native or decoded image (if it requires external codec decoding).
- Uri sourceUri = new(path);
- (sourceUri, _) = await ImageBackgroundManager
- .GetNativeOrDecodedImagePath(sourceUri, CancellationToken.None);
-
- using CanvasBitmap bitmap = sourceUri.IsFile ?
- await CanvasBitmap.LoadAsync(canvasDevice,
- sourceUri.LocalPath,
- 96f,
- CanvasAlphaMode.Premultiplied) :
- await CanvasBitmap.LoadAsync(canvasDevice,
- sourceUri,
- 96f,
- CanvasAlphaMode.Premultiplied);
-
- // Load to render target
- return await Task.Run(() =>
- {
- CanvasRenderTarget renderTarget = new(canvasDevice,
- bitmap.SizeInPixels.Width,
- bitmap.SizeInPixels.Height, bitmap.Dpi);
- using CanvasDrawingSession ds = renderTarget.CreateDrawingSession();
- ds.DrawImage(bitmap);
- return renderTarget;
- });
- }
-
- private static void MergeCanvasTargetWith(CanvasRenderTarget sourceCanvasTarget,
- CanvasRenderTarget? withCanvasTarget)
- {
- if (withCanvasTarget == null)
- {
- return; // Abort
- }
-
- CanvasDrawingSession? ds = sourceCanvasTarget.CreateDrawingSession();
- ds.DrawImage(withCanvasTarget);
-
- // Dispose the overlay canvas
- withCanvasTarget.Dispose();
-
- if (DispatcherQueueExtensions.CurrentDispatcherQueue.HasThreadAccess)
- {
- ds.Dispose();
- return;
- }
-
- DispatcherQueueExtensions.CurrentDispatcherQueue.TryEnqueue(DispatcherQueuePriority.High, ds.Dispose);
}
private static double GetBitmapBitSize(DirectXPixelFormat pixelFormat)
@@ -300,9 +159,7 @@ private static async Task WriteCanvasTargetBufferType(
private static async Task CopyCanvasTargetBufferToClipboard(CanvasRenderTarget? sourceCanvasTarget)
{
if (sourceCanvasTarget == null)
- {
return;
- }
bool isClipboardOpened = true;
byte[]? buffer = null;
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs
index da68852c83..ab907d7c89 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.CodecDetect.cs
@@ -1,13 +1,16 @@
using CollapseLauncher.Dialogs;
+using CollapseLauncher.Helper.InternalPInvoke;
+using CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
using CollapseLauncher.XAMLs.Theme.CustomControls;
using Hi3Helper.Win32.WinRT.WindowsCodec;
using System;
using System.IO;
using System.Threading.Tasks;
-
+// ReSharper disable CommentTypo
// ReSharper disable IdentifierTypo
// ReSharper disable StringLiteralTypo
// ReSharper disable CheckNamespace
+#pragma warning disable IDE0130
#nullable enable
namespace CollapseLauncher.GameManagement.ImageBackground;
@@ -16,52 +19,56 @@ public partial class ImageBackgroundManager
{
#region Codec Checks
- private async ValueTask<(bool IsSupported, bool IsVideo)> CheckCodecOrSpawnDialog(Uri? fileUri)
+ private async ValueTask<(bool IsSupported, bool ForceFFmpeg)> CheckCodecOrSpawnDialog(string? localPath)
{
- // -- Cancel if null
- if (fileUri == null)
- {
+ if (localPath == null)
return (false, false);
- }
-
- string filePath = fileUri.IsFile ? fileUri.LocalPath : fileUri.ToString();
// -- Check for supported extension first
- if (!IsMediaFileExtensionSupported(filePath))
+ if (!IsMediaFileExtensionSupported(localPath))
{
- await SimpleDialogs.Dialog_SpawnMediaExtensionNotSupportedDialog(filePath);
+ await SimpleDialogs.Dialog_SpawnMediaExtensionNotSupportedDialog(localPath);
return (false, false);
}
// -- Check for supported image codec
- if (IsImageMediaFileExtensionSupported(filePath))
+ if (IsImageMediaFileExtensionSupported(localPath))
{
- if (WindowsCodecHelper.IsFileSupportedImage(filePath))
+ if (WindowsCodecHelper.IsFileSupportedImage(localPath))
{
return (true, false);
}
- await SimpleDialogs.Dialog_SpawnImageNotSupportedDialog(filePath);
+ await SimpleDialogs.Dialog_SpawnImageNotSupportedDialog(localPath);
return (false, false);
}
+ // -- Check using FFmpeg if available
+ MediaSupport codecInfo = FFmpegPInvoke.GetMediaSupport(localPath);
+ PixelColorModel pixelFormat = codecInfo.VideoPixelFormatInfo.ColorModel;
+ if ((GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable) ||
+ (codecInfo.Video.DecoderAvailable && pixelFormat.HasFlag(PixelColorModel.Rgb)))
+ {
+ return (true, true);
+ }
+
// -- Check for supported video codec
- if (WindowsCodecHelper.IsFileSupportedVideo(filePath,
+ if (WindowsCodecHelper.IsFileSupportedVideo(localPath,
out bool canPlayVideo,
out bool canPlayAudio,
out Guid videoCodecGuid,
out Guid audioCodecGuid) ||
(GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable))
{
- return (true, true);
+ return (true, false);
}
return (await SimpleDialogs
- .Dialog_SpawnVideoNotSupportedDialog(filePath,
+ .Dialog_SpawnVideoNotSupportedDialog(localPath,
canPlayVideo,
canPlayAudio,
videoCodecGuid,
- audioCodecGuid), true);
+ audioCodecGuid), false);
}
#endregion
@@ -80,7 +87,7 @@ private static bool IsImageMediaFileExtensionSupported(ReadOnlySpan filePa
LayeredBackgroundImage.SupportedImageVectorExtensionsLookup.Contains(extension);
}
- private static bool IsVideoMediaFileExtensionSupported(ReadOnlySpan filePath)
+ public static bool IsVideoMediaFileExtensionSupported(ReadOnlySpan filePath)
{
ReadOnlySpan extension = Path.GetExtension(filePath);
return LayeredBackgroundImage.SupportedVideoExtensionsLookup.Contains(extension);
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Control.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Control.cs
index 58f6c6ab97..1492aae22f 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Control.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Control.cs
@@ -1,7 +1,4 @@
-using CollapseLauncher.XAMLs.Theme.CustomControls;
-using System.Threading;
-
-// ReSharper disable ConvertIfStatementToSwitchStatement
+// ReSharper disable ConvertIfStatementToSwitchStatement
// ReSharper disable CheckNamespace
#pragma warning disable IDE0130
@@ -10,61 +7,56 @@ namespace CollapseLauncher.GameManagement.ImageBackground;
public partial class ImageBackgroundManager
{
- private bool _isPausedByUser;
-
public void SetWindowFocusedEvent()
{
- if (GlobalKeepPlayVideoWhenWindowUnfocused)
- {
- CurrentBackgroundElement?.FadeInAudio();
- }
- else
+ if (!GlobalKeepPlayVideoWhenWindowUnfocused)
{
Play(false);
+ return;
}
+
+ CurrentBackgroundElement?.SetVolume(GlobalBackgroundAudioVolume / 100d);
}
public void SetWindowUnfocusedEvent()
{
- if (GlobalKeepPlayVideoWhenWindowUnfocused)
- {
- CurrentBackgroundElement?.FadeOutAudio();
- }
- else
+ if (!GlobalKeepPlayVideoWhenWindowUnfocused)
{
Pause(false);
+ return;
}
+
+ CurrentBackgroundElement?.SetVolume(0);
}
- public void SetWindowMinimizeEvent() => Pause(false);
+ public void SetWindowMinimizeEvent()
+ {
+ CurrentBackgroundElement?.ReleaseViewAllocation();
+ }
- public void SetWindowRestoreEvent() => Play(false);
+ public void SetWindowRestoreEvent()
+ {
+ // Trynna spawn the background element if not loaded from first start-up.
+ if (CurrentBackgroundElement == null)
+ LoadImage();
+
+ CurrentBackgroundElement?.RestoreViewAllocation();
+ }
public void Play(bool isUserRequest = true)
{
- // Block play request for window event if paused by user
- if (!isUserRequest && (_isPausedByUser || !CurrentIsEnableBackgroundAutoPlay))
- {
- return;
- }
-
if (isUserRequest)
{
CurrentIsEnableBackgroundAutoPlay = true;
}
-
- // Alter to load static image if it has one instead of pausing.
- LayeredImageBackgroundContext? context = CurrentSelectedBackgroundContext;
- bool hasStaticImage = !string.IsNullOrEmpty(context?.BackgroundImageStaticPath);
- if (isUserRequest && hasStaticImage && context != null)
+
+ // Alter to restore video if it has static image.
+ if (isUserRequest && !string.IsNullOrEmpty(CurrentSelectedBackgroundContext?.OriginPlaceholderImagePath))
{
- LoadImageAtIndex(CurrentSelectedBackgroundIndex, false, CancellationToken.None, true);
+ LoadImage();
+ return;
}
- // Force to restore autoplay status to true.
- CurrentBackgroundElement?.SetValue(LayeredBackgroundImage.IsVideoAutoplayProperty, true);
-
- Interlocked.Exchange(ref _isPausedByUser, false);
CurrentBackgroundElement?.Play();
}
@@ -72,16 +64,13 @@ public void Pause(bool isUserRequest = true)
{
if (isUserRequest)
{
- Interlocked.Exchange(ref _isPausedByUser, true);
CurrentIsEnableBackgroundAutoPlay = false;
}
-
+
// Alter to load static image if it has one instead of pausing.
- LayeredImageBackgroundContext? context = CurrentSelectedBackgroundContext;
- bool hasStaticImage = !string.IsNullOrEmpty(context?.BackgroundImageStaticPath);
- if (isUserRequest && hasStaticImage && context != null)
+ if (isUserRequest && !string.IsNullOrEmpty(CurrentSelectedBackgroundContext?.OriginPlaceholderImagePath))
{
- LoadImageAtIndex(CurrentSelectedBackgroundIndex, true, CancellationToken.None, true);
+ LoadImage();
return;
}
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.FFmpeg.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.FFmpeg.cs
index 7338a8a66c..74f688ba40 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.FFmpeg.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.FFmpeg.cs
@@ -1,14 +1,13 @@
using CollapseLauncher.Helper;
-using CollapseLauncher.Helper.FFmpegPInvoke;
+using CollapseLauncher.Helper.InternalPInvoke;
+using CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
using CollapseLauncher.Helper.StreamUtility;
using FFmpegInteropX;
using Hi3Helper;
using Hi3Helper.Shared.Region;
using System;
-using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.IO;
-using System.Linq;
using System.Runtime.CompilerServices;
// ReSharper disable StringLiteralTypo
// ReSharper disable IdentifierTypo
@@ -23,70 +22,44 @@ public partial class ImageBackgroundManager
#region Shared/Static Properties and Fields
private const string GlobalIsUseFFmpegConfigKey = "GlobalIsUseFFmpeg";
- private const string GlobalFFmpegVersionToUseConfigKey = "GlobalFFmpegVersionToUse";
- private const string GlobalFFmpegCustomPathConfigKey = "GlobalFFmpegCustomPath";
private const string GlobalFFmpegDecodingModeConfigKey = "GlobalFFmpegDecodingMode";
- private const int GlobalFFmpegDefaultVersionKey = 80;
-
public VideoDecoderMode[] AvailableFFmpegDecodingModes => field ??= Enum.GetValues();
public int GlobalFFmpegVersionToUse
{
- get
- {
- int version = LauncherConfig.GetAppConfigValue(GlobalFFmpegVersionToUseConfigKey);
- return FFmpegPInvoke.FFmpegVersionLibNames.ContainsKey(version) ?
- version :
- GlobalFFmpegDefaultVersionKey;
- }
+ get => FFmpegPInvoke.FFmpegVersionToUse;
set
{
- if (!FFmpegPInvoke.FFmpegVersionLibNames.ContainsKey(value))
- {
- return;
- }
-
- LauncherConfig.SetAndSaveConfigValue(GlobalFFmpegVersionToUseConfigKey, value);
+ FFmpegPInvoke.FFmpegVersionToUse = value;
OnPropertyChanged();
OnPropertyChanged(nameof(GlobalFFmpegLibraryNames)); // Notify FFmpeg library names update too.
}
}
- public FFmpegPInvoke.FFmpegLibraryNames GlobalFFmpegLibraryNames
- {
- get
- {
- if (FFmpegPInvoke.FFmpegVersionLibNames.TryGetValue(GlobalFFmpegVersionToUse, out FFmpegPInvoke.FFmpegLibraryNames names))
- {
- return names;
- }
-
- return FFmpegPInvoke.FFmpegVersionLibNames.Values.FirstOrDefault();
- }
- }
+ public FFmpegLibraryNames GlobalFFmpegLibraryNames => FFmpegPInvoke.FFmpegLibraryNamings;
- public bool GlobalIsUseFFmpeg
+ public string? GlobalCustomFFmpegPath
{
- get => LauncherConfig.GetAppConfigValue(GlobalIsUseFFmpegConfigKey);
+ get => FFmpegPInvoke.CustomFFmpegPath;
set
{
- LauncherConfig.SetAndSaveConfigValue(GlobalIsUseFFmpegConfigKey, value);
+ FFmpegPInvoke.CustomFFmpegPath = value;
OnPropertyChanged();
}
}
- public string? GlobalCustomFFmpegPath
+ public bool GlobalIsUseFFmpeg
{
- get => LauncherConfig.GetAppConfigValue(GlobalFFmpegCustomPathConfigKey);
+ get => LauncherConfig.GetAppConfigValue(GlobalIsUseFFmpegConfigKey);
set
{
- LauncherConfig.SetAndSaveConfigValue(GlobalFFmpegCustomPathConfigKey, value);
+ LauncherConfig.SetAndSaveConfigValue(GlobalIsUseFFmpegConfigKey, value);
OnPropertyChanged();
}
}
- public bool GlobalIsFFmpegAvailable => IsFFmpegAvailable(Directory.GetCurrentDirectory(), GlobalFFmpegLibraryNames, out _);
+ public bool GlobalIsFFmpegAvailable => FFmpegPInvoke.IsFFmpegAvailable(Directory.GetCurrentDirectory(), GlobalFFmpegLibraryNames, out _);
public bool GlobalIsFFmpegCurrentlyUsed
{
@@ -144,17 +117,17 @@ public bool TryRelinkFFmpegPath()
return false;
}
- FFmpegPInvoke.FFmpegLibraryNames names = GlobalFFmpegLibraryNames;
+ FFmpegLibraryNames names = GlobalFFmpegLibraryNames;
string curDir = LauncherConfig.AppExecutableDir;
string stockDir = Path.Combine(curDir, "Lib");
- string? stockFindDir = FindFFmpegInstallFolder(stockDir, names);
+ string? stockFindDir = FFmpegPInvoke.FindFFmpegInstallFolder(stockDir, names);
string? customFFmpegDirPath = GlobalCustomFFmpegPath;
// -- 1. Check from custom path first. If it exists, then pass.
if (!string.IsNullOrEmpty(customFFmpegDirPath) &&
- IsFFmpegAvailable(customFFmpegDirPath, names, out exception) &&
+ FFmpegPInvoke.IsFFmpegAvailable(customFFmpegDirPath, names, out exception) &&
TryLinkFFmpegLibrary(customFFmpegDirPath, curDir, names, out exception))
{
return result = true;
@@ -162,7 +135,7 @@ public bool TryRelinkFFmpegPath()
// -- 2. Link stock library to the root directory
if (!string.IsNullOrEmpty(stockFindDir) &&
- IsFFmpegAvailable(stockFindDir, names, out exception) &&
+ FFmpegPInvoke.IsFFmpegAvailable(stockFindDir, names, out exception) &&
TryLinkFFmpegLibrary(stockFindDir, curDir, names, out exception))
{
return result = true;
@@ -170,8 +143,14 @@ public bool TryRelinkFFmpegPath()
// -- 3. Find one from environment variables. If it exists, then pass.
// Otherwise, return false.
- return result = TryFindFFmpegInstallFromEnvVar(names, out string? envVarPath, out exception) &&
- TryLinkFFmpegLibrary(envVarPath, curDir, names, out exception);
+ if (!FFmpegPInvoke.TryFindFFmpegInstallFromEnvVar(names, out string? envVarPath, out exception) ||
+ !TryLinkFFmpegLibrary(envVarPath, curDir, names, out exception))
+ {
+ return false;
+ }
+
+ GlobalCustomFFmpegPath = envVarPath; // Set as custom path
+ return true;
}
finally
{
@@ -184,81 +163,9 @@ public bool TryRelinkFFmpegPath()
}
}
- internal bool TryFindFFmpegInstallFromEnvVar(FFmpegPInvoke.FFmpegLibraryNames libraries, [NotNullWhen(true)] out string? path, out Exception? exception)
- {
- return FindIn(EnvironmentVariableTarget.User, out path, out exception) ||
- FindIn(EnvironmentVariableTarget.Machine, out path, out exception);
-
- bool FindIn(EnvironmentVariableTarget target, [NotNullWhen(true)] out string? innerPath, out Exception? exception)
- {
- const string separators = ";,";
- Unsafe.SkipInit(out innerPath);
- Unsafe.SkipInit(out exception);
-
- foreach (object? varValue in Environment.GetEnvironmentVariables(target))
- {
- if (varValue is not DictionaryEntry { Value: string varValueStr })
- {
- continue;
- }
-
- ReadOnlySpan envVar = varValueStr;
- foreach (Range envVarRange in envVar.SplitAny(separators))
- {
- ReadOnlySpan envVarPath = envVar[envVarRange].Trim(" '\"");
- if (envVarPath.IsEmpty)
- {
- continue;
- }
-
- string thisPath = envVarPath.ToString();
-
- if (!Path.IsPathFullyQualified(thisPath) ||
- !IsFFmpegAvailable(thisPath, libraries, out exception)) continue;
-
- innerPath = thisPath;
- GlobalCustomFFmpegPath = thisPath; // Set as custom path
- return true;
- }
- }
-
- return false;
- }
- }
-
- internal static bool IsFFmpegAvailable(string? checkOnDirectory,
- FFmpegPInvoke.FFmpegLibraryNames libraries,
- [NotNullWhen(false)]
- out Exception? exception)
- {
- if (string.IsNullOrEmpty(checkOnDirectory))
- {
- exception = new NullReferenceException($"Argument: {nameof(checkOnDirectory)} is null!");
- return false;
- }
-
- checkOnDirectory = FileUtility.GetFullyQualifiedPath(checkOnDirectory);
-
- string dllPathAvcodec = Path.Combine(checkOnDirectory, libraries.Codec);
- string dllPathAvdevice = Path.Combine(checkOnDirectory, libraries.Device);
- string dllPathAvfilter = Path.Combine(checkOnDirectory, libraries.Filter);
- string dllPathAvformat = Path.Combine(checkOnDirectory, libraries.Format);
- string dllPathAvutil = Path.Combine(checkOnDirectory, libraries.Util);
- string dllPathSwresample = Path.Combine(checkOnDirectory, libraries.Resample);
- string dllPathSwscale = Path.Combine(checkOnDirectory, libraries.Scale);
-
- return FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvcodec, out _, out exception) &&
- FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvdevice, out _, out exception) &&
- FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvfilter, out _, out exception) &&
- FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvformat, out _, out exception) &&
- FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvutil, out _, out exception) &&
- FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathSwresample, out _, out exception) &&
- FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathSwscale, out _, out exception);
- }
-
internal static string[] GetFFmpegRequiredDllFilenames()
{
- FFmpegPInvoke.FFmpegLibraryNames names = Shared.GlobalFFmpegLibraryNames;
+ FFmpegLibraryNames names = Shared.GlobalFFmpegLibraryNames;
return [
names.Codec,
names.Device,
@@ -270,35 +177,10 @@ internal static string[] GetFFmpegRequiredDllFilenames()
];
}
- internal static string? FindFFmpegInstallFolder(string checkOnDirectory, FFmpegPInvoke.FFmpegLibraryNames libraries)
- {
- try
- {
- if (IsFFmpegAvailable(checkOnDirectory, libraries, out _))
- {
- return checkOnDirectory;
- }
-
- foreach (string dirPath in FileUtility.EnumerateDirectoryRecursive(checkOnDirectory))
- {
- if (IsFFmpegAvailable(dirPath, libraries, out _))
- {
- return dirPath;
- }
- }
- }
- catch
- {
- // ignored
- }
-
- return null;
- }
-
public static bool TryLinkFFmpegLibrary(
- string? sourceDir,
- string? targetDir,
- FFmpegPInvoke.FFmpegLibraryNames libraries,
+ string? sourceDir,
+ string? targetDir,
+ FFmpegLibraryNames libraries,
[NotNullWhen(false)]
out Exception? exception)
{
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.ImageCropperAndConvert.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.ImageCropperAndConvert.cs
index dca17f19f6..6e16b4acd7 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.ImageCropperAndConvert.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.ImageCropperAndConvert.cs
@@ -1,6 +1,7 @@
using CollapseLauncher.Dialogs;
using CollapseLauncher.Extension;
using CollapseLauncher.Helper;
+using CollapseLauncher.Helper.Image;
using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.XAMLs.Theme.ContentDialog;
using CollapseLauncher.XAMLs.Theme.CustomControls;
@@ -35,6 +36,39 @@ namespace CollapseLauncher.GameManagement.ImageBackground;
public partial class ImageBackgroundManager
{
+ private readonly struct GetCroppedResult
+ {
+ public readonly string? ProcessedOverlayPath;
+ public readonly string? ProcessedBackgroundPath;
+ public readonly bool IsCancelResize;
+ public readonly bool WasCropPerformed;
+
+ private GetCroppedResult(string? processedOverlayPath,
+ string? processedBackgroundPath,
+ bool isCancelResize,
+ bool wasCropPerformed)
+ {
+ ProcessedOverlayPath = processedOverlayPath;
+ ProcessedBackgroundPath = processedBackgroundPath;
+ IsCancelResize = isCancelResize;
+ WasCropPerformed = wasCropPerformed;
+ }
+
+ public static GetCroppedResult Create(string? processedOverlayPath,
+ string? processedBackgroundPath,
+ bool isCancelResize,
+ bool wasCropPerformed)
+ {
+ return new GetCroppedResult(processedOverlayPath,
+ processedBackgroundPath,
+ isCancelResize,
+ wasCropPerformed);
+ }
+
+ public static readonly GetCroppedResult Cancelled = Create(null, null, true, false);
+ public static readonly GetCroppedResult Skip = Create(null, null, false, false);
+ }
+
///
/// Get local path of the resized/cropped custom image. The return values are cropped image path.
/// If the input path is a video, then it will return the input path.
@@ -42,7 +76,7 @@ public partial class ImageBackgroundManager
/// To perform cropping request, you must set to .
/// Otherwise, it will return the input paths.
///
- private static async Task<(string? ProcessedOverlayPath, string? ProcessedBackgroundPath, bool IsCancelResize)>
+ private static async Task
GetCroppedCustomImage(string? overlayUrlOrPath,
string? backgroundUrlOrPath,
bool performCropRequest,
@@ -52,21 +86,21 @@ public partial class ImageBackgroundManager
string.IsNullOrEmpty(backgroundUrlOrPath)) ||
string.IsNullOrEmpty(backgroundUrlOrPath))
{
- return (null, null, false); // Skip from processing
+ return GetCroppedResult.Skip;
}
// -- Check if the file is video or image
if (LayeredBackgroundImage.SupportedVideoExtensionsLookup
.Contains(Path.GetExtension(backgroundUrlOrPath)))
{
- return (overlayUrlOrPath, backgroundUrlOrPath, false);
+ return GetCroppedResult.Create(overlayUrlOrPath, backgroundUrlOrPath, false, false);
}
// -- Try to get existing image file path
if (TryGetCroppedImageFilePath(backgroundUrlOrPath, out string croppedBackgroundPath) &&
!performCropRequest)
{
- return (null, croppedBackgroundPath, false);
+ return GetCroppedResult.Create(null, croppedBackgroundPath, false, false);
}
try
@@ -90,26 +124,21 @@ public partial class ImageBackgroundManager
Uri backgroundImageUri = new(decodedBackgroundPath);
// -- Create elements, load image and spawn the dialog box.
- Grid parentGrid = UIElementExtensions
- .Create(SetParentGridProperties);
-
- ContentDialogOverlay dialog = UIElementExtensions
- .Create(SetDialogProperties);
-
- ImageCropper cropper = UIElementExtensions
- .Create(SetImageCropperProperties);
+ Grid parentGrid = UIElementExtensions.Create(SetParentGridProperties);
+ ContentDialogOverlay dialog = UIElementExtensions.Create(SetDialogProperties);
+ ImageCropper cropper = UIElementExtensions.Create(SetImageCropperProperties);
// Skip spawning crop overlay when source dimensions do not meet minimum crop size.
if (!await CanOpenCropOverlay(backgroundImageUri, backgroundCodecType == ImageExternalCodecType.Svg, cropper, token))
{
- return (overlayUrlOrPath, backgroundUrlOrPath, false);
+ return GetCroppedResult.Create(overlayUrlOrPath, backgroundUrlOrPath, false, false);
}
// -- Register to close dialog if cancellation is triggered outside the event.
token.Register(dialog.Hide);
if (token.IsCancellationRequested)
{
- return (null, null, true);
+ return GetCroppedResult.Cancelled;
}
DispatcherQueueExtensions
@@ -124,29 +153,18 @@ public partial class ImageBackgroundManager
ContentDialogResult dialogResult = await dialog.QueueAndSpawnDialog();
if (dialogResult is ContentDialogResult.Secondary or ContentDialogResult.None)
{
- return (null, null, true);
+ return GetCroppedResult.Cancelled;
}
// -- Save cropped image
await SaveCroppedImageToFilePath(backgroundUrlOrPath, croppedBackgroundPath, cropper, token);
// -- If there's an upscaled image, delete it.
- if (TryGetUpscaledFilePath(croppedBackgroundPath,
- out string croppedImagePath))
- {
- try
- {
- File.Delete(croppedImagePath);
- }
- catch (Exception ex)
- {
- Logger.LogWriteLine($"An error has occurred while trying to delete previous upscaled image: {ex}",
- LogType.Error,
- true);
- }
- }
+ if (TryGetUpscaledFilePath(croppedBackgroundPath, out string croppedImagePath))
+ StreamExtension.TryDeleteFile(croppedImagePath);
+
- return (null, croppedBackgroundPath, false);
+ return GetCroppedResult.Create(null, croppedBackgroundPath, false, true);
void SetDialogProperties(ContentDialogOverlay element)
{
@@ -189,7 +207,73 @@ void SetImageCropperProperties(ImageCropper element)
}
catch (OperationCanceledException)
{
- return (null, null, true);
+ return GetCroppedResult.Cancelled;
+ }
+ }
+
+ public static bool TryGetUpscaledFilePath(string inputFilePath,
+ out string upscaledFilePath)
+ {
+ upscaledFilePath = Path.Combine(LauncherConfig.AppGameImgCachedFolder, $"scaled_{Path.GetFileNameWithoutExtension(inputFilePath)}.png");
+ if (LayeredBackgroundImage.SupportedVideoExtensionsLookup.Contains(Path.GetExtension(inputFilePath))) // Ignore if file is a video.
+ {
+ upscaledFilePath = inputFilePath;
+ return true;
+ }
+
+ FileInfo outputFileInfo = new(upscaledFilePath);
+ return outputFileInfo is { Exists: true, Length: > 0 };
+ }
+
+ public static async Task TryGetScaledWaifu2XImagePath(Uri uri, CancellationToken token)
+ {
+ if (uri == null!)
+ {
+ return null!;
+ }
+
+ if (!uri.IsFile)
+ {
+ throw new InvalidOperationException("You must provide a local file path!");
+ }
+
+ // Ignore if the input file is a video
+ string inputFilePath = uri.LocalPath;
+ if (TryGetUpscaledFilePath(inputFilePath, out string outputFilePath))
+ {
+ return new Uri(outputFilePath);
+ }
+
+ FileInfo outputFileInfo = new(outputFilePath);
+ outputFileInfo.Directory?.Create();
+
+ await Task.Run(Impl, token);
+ outputFileInfo.Refresh();
+
+ try
+ {
+ token.ThrowIfCancellationRequested();
+ return new Uri(outputFileInfo.FullName);
+ }
+ catch
+ {
+ outputFileInfo.TryDeleteFile();
+ throw;
+ }
+
+ void Impl()
+ {
+ using ProcessingPipeline pipeline =
+ MagicImageProcessor
+ .BuildPipeline(inputFilePath,
+ ProcessImageSettings.Default);
+
+ using FileStream outputFileStream = outputFileInfo.Open(FileMode.Create,
+ FileAccess.Write,
+ FileShare.ReadWrite);
+
+ pipeline.AddTransform(new Waifu2XTransform(ImageLoaderHelper.Waifu2X));
+ pipeline.WriteOutput(outputFileStream);
}
}
@@ -382,7 +466,7 @@ private static async Task ConvertImageToPngFromStream(
///
/// Converts any (seekable) image stream to PNG.
///
- private static Task ConvertImageToPngFromStream(
+ private static async Task ConvertImageToPngFromStream(
Stream sourceStream,
Stream targetStream,
Rectangle cropArea = default,
@@ -394,7 +478,8 @@ private static Task ConvertImageToPngFromStream(
IsBackground = true
}.Start((sourceStream, targetStream, tcs, token, cropArea));
- return tcs.Task;
+ await tcs.Task;
+ return;
static void Impl(object? state)
{
@@ -407,6 +492,8 @@ static void Impl(object? state)
try
{
+ innerSourceStream.Position = 0;
+
// We won't do any post-processing and just do a straight conversion to PNG.
// The post-processing (like resizing with Waifu2X) will be done on image loading process.
ProcessImageSettings settings = new()
@@ -478,40 +565,51 @@ private static bool IsWebpExtendedFormat(Stream stream, out ColorProfileMode col
/// Get native or redirected decoded image path.
///
/// If the codec of the image is not supported.
- internal static async Task<(string, ImageExternalCodecType)> GetNativeOrDecodedImagePath(string filePath, CancellationToken token)
+ internal static ValueTask<(string, ImageExternalCodecType)> GetNativeOrDecodedImagePath(string filePath, CancellationToken token = default)
{
// Try to get decoded temporary file. If it exists, return the file path.
if (TryGetDecodedTemporaryFile(filePath, out string decodedFilePath))
{
- return (decodedFilePath, ImageExternalCodecType.Default);
+ return new ValueTask<(string, ImageExternalCodecType)>((decodedFilePath, ImageExternalCodecType.Default));
}
- // Check the extension type. If the type is native or a video file, then just return the original path.
- if (await TryGetImageCodecType(filePath, token) is var codecType &&
- codecType is ImageExternalCodecType.Default or ImageExternalCodecType.Svg)
+ Task<(string, ImageExternalCodecType)> processTask = ImplAsync();
+ if (processTask.IsCompleted)
{
- return (filePath, codecType);
+ return new ValueTask<(string, ImageExternalCodecType)>(processTask.Result);
}
- // Return null if the codec type is not supported.
- if (codecType == ImageExternalCodecType.NotSupported)
+ return new ValueTask<(string, ImageExternalCodecType)>(processTask);
+
+ async Task<(string, ImageExternalCodecType)> ImplAsync()
{
- throw new NotSupportedException($"The format of the image: {filePath} is not supported!");
- }
+ // Check the extension type. If the type is native or a video file, then just return the original path.
+ if (await TryGetImageCodecType(filePath, token) is var codecType &&
+ codecType is ImageExternalCodecType.Default or ImageExternalCodecType.Svg)
+ {
+ return (filePath, codecType);
+ }
- // Otherwise, try to convert the image to png and write it to given decoded file path.
- await using Stream sourceStream = await OpenStreamFromFileOrUrl(filePath, token);
- await ConvertImageToPngFromStream(sourceStream, decodedFilePath, token: token);
+ // Return null if the codec type is not supported.
+ if (codecType == ImageExternalCodecType.NotSupported)
+ {
+ throw new NotSupportedException($"The format of the image: {filePath} is not supported!");
+ }
+
+ // Otherwise, try to convert the image to png and write it to given decoded file path.
+ await using Stream sourceStream = await OpenStreamFromFileOrUrl(filePath, token);
+ await ConvertImageToPngFromStream(sourceStream, decodedFilePath, token: token);
- return (decodedFilePath, codecType);
+ return (decodedFilePath, codecType);
+ }
}
///
/// Get native or redirected decoded image path.
///
/// If the codec of the image is not supported.
- internal static async Task<(Uri, ImageExternalCodecType)> GetNativeOrDecodedImagePath(
- Uri filePath, CancellationToken token)
+ internal static async ValueTask<(Uri, ImageExternalCodecType)> GetNativeOrDecodedImagePath(
+ Uri filePath, CancellationToken token = default)
{
(string, ImageExternalCodecType) result = await GetNativeOrDecodedImagePath(filePath.ToString(), token);
return (new Uri(result.Item1), result.Item2);
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs
index 3441f50acd..1c93090f12 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.Loaders.cs
@@ -1,34 +1,21 @@
using CollapseLauncher.Extension;
using CollapseLauncher.Helper;
using CollapseLauncher.Helper.Background;
-using CollapseLauncher.Helper.Image;
-using CollapseLauncher.Helper.Metadata;
-using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.Pages;
using CollapseLauncher.XAMLs.Theme.CustomControls;
using Hi3Helper;
using Hi3Helper.SentryHelper;
using Hi3Helper.Shared.Region;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Data;
-using Microsoft.UI.Xaml.Media;
-using Microsoft.UI.Xaml.Media.Animation;
-using Microsoft.UI.Xaml.Media.Imaging;
-using PhotoSauce.MagicScaler;
using System;
-using System.Collections.Generic;
using System.Diagnostics;
-using System.Globalization;
-using System.IO;
-using System.Runtime.InteropServices;
-using System.Linq;
-using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Windows.UI;
// ReSharper disable IdentifierTypo
// ReSharper disable CheckNamespace
+// ReSharper disable CommentTypo
#nullable enable
namespace CollapseLauncher.GameManagement.ImageBackground;
@@ -39,626 +26,254 @@ public partial class ImageBackgroundManager
private CancellationTokenSource? _imageLoadingTokenSource;
+ private static readonly MinMaxToPercentConverter MinMaxToPercentConverter = new(0d, 100d);
+
+ private string? _lastBackgroundSource;
+
#endregion
- private void LoadImageAtIndex(int index, bool forceLoadToStatic, CancellationToken token, bool forceReload = false)
+ private void LoadImage(CancellationToken token = default)
{
- if (ImageContextSources.Count <= index ||
- index < 0)
+ if (ImageContextSources.Count == 0 ||
+ CurrentSelectedBackgroundIndex < 0 ||
+ PresenterGrid == null)
{
return;
}
- if (IsBackgroundLoading)
- {
- CancelCurrentBackgroundLoad();
- }
+ new Thread(Impl) { IsBackground = true }.Start();
+ return;
- // Only show loading indicator when a download is required.
- if (!CheckIsContextCached(ImageContextSources[index]))
- {
- IsBackgroundLoading = true;
- }
- new Thread(async void () =>
+ async void Impl()
{
+ Stopwatch? stopwatch = null;
try
{
- await LoadImageAtIndexCore(index, forceLoadToStatic, token, forceReload).ConfigureAwait(false);
- }
- catch (Exception e)
- {
- Logger.LogWriteLine($"{e}", LogType.Error, true);
- }
- })
- {
- IsBackground = true,
- Priority = ThreadPriority.Lowest
- }.UnsafeStart();
- }
+ // -- Get context and invalidate previous CTS
+ LayeredImageBackgroundContext? context = CurrentSelectedBackgroundContext;
+ if (context == null)
+ return;
- private void CancelCurrentBackgroundLoad()
- {
- if (Interlocked.Exchange(ref _imageLoadingTokenSource, null) is { } lastCts)
- {
- lastCts.Cancel();
- lastCts.Dispose();
- }
- IsBackgroundLoading = false;
- }
+ // -- Invalidate last Cancellation Token (and thus, cancel all asynchronous routines)
+ // and then renew the token. Also toggle the background loading indicator.
+ DispatcherQueueExtensions.CurrentDispatcherQueue.TryEnqueue(() => IsBackgroundLoading = true );
+ token = InvalidateAndRenewCancelToken(token);
- public void PreloadBackground(PresetConfig? preset, Grid? presenterGrid)
- {
- if (preset == null || presenterGrid == null) return;
+ stopwatch = Stopwatch.StartNew();
- CancelCurrentBackgroundLoad();
- Interlocked.Increment(ref _loadGeneration);
+ // -- Notify changes on context menu properties
+ DispatcherQueueExtensions.CurrentDispatcherQueue.TryEnqueue(() => OnPropertyChanged(nameof(CurrentSelectedBackgroundHasOverlayImage)));
- PresenterGrid = presenterGrid;
- string autoPlayKey = $"LastIsEnableBackgroundAutoPlay-{preset.GameName}-{preset.ZoneName}";
- CurrentIsEnableBackgroundAutoPlayKey = autoPlayKey;
+ // -- Start clean-up if force reload requested
+ if (context.ForceReload)
+ {
+ context.DeleteCachedImagePath(context.OriginOverlayImagePath);
+ context.DeleteCachedImagePath(context.OriginBackgroundImagePath);
+ context.DeleteCachedImagePath(context.OriginPlaceholderImagePath);
+ }
- string cachedBgKey = $"CachedBg-{preset.GameName}-{preset.ZoneName}";
- string? backgroundUrl = LauncherConfig.GetAppConfigValue($"{cachedBgKey}-Background").ToString();
- string? staticBgUrl = LauncherConfig.GetAppConfigValue($"{cachedBgKey}-StaticBackground").ToString();
- string? overlayUrl = LauncherConfig.GetAppConfigValue($"{cachedBgKey}-Overlay").ToString();
+ // -- Get Cached path.
+ string? downloadedOverlayUri = null;
+ string? downloadedBackgroundUri = null;
+ string? downloadedPlaceholderUri = null;
- if (string.IsNullOrEmpty(backgroundUrl) && string.IsNullOrEmpty(staticBgUrl))
- {
- IsBackgroundLoading = true;
- return;
- }
+ Task overlayLoadTask = context.GetCachedImagePathAsync(context.OriginOverlayImagePath, token).GetResultFromAction(result => downloadedOverlayUri = result);
+ Task backgroundLoadTask = context.GetCachedImagePathAsync(context.OriginBackgroundImagePath, token).GetResultFromAction(result => downloadedBackgroundUri = result);
+ Task placeholderLoadTask = context.GetCachedImagePathAsync(context.OriginPlaceholderImagePath, token).GetResultFromAction(result => downloadedPlaceholderUri = result);
- string? primaryBg = backgroundUrl ?? staticBgUrl;
+ await Task.WhenAll(overlayLoadTask,
+ backgroundLoadTask,
+ placeholderLoadTask);
- LayeredImageBackgroundContext context = new()
- {
- OriginOverlayImagePath = overlayUrl,
- OriginBackgroundImagePath = backgroundUrl,
- OriginBackgroundStaticImagePath = staticBgUrl,
- OverlayImagePath = overlayUrl,
- BackgroundImagePath = primaryBg,
- BackgroundImageStaticPath = staticBgUrl,
- IsVideo = IsVideoMediaFileExtensionSupported(primaryBg ?? staticBgUrl!),
- IsCustom = false
- };
-
- if (CheckIsContextCached(context))
- {
- string? localOverlay = TryGetCachedLocalPath(overlayUrl);
- string? localBackground = TryGetCachedLocalPath(primaryBg);
- string? localStatic = TryGetCachedLocalPath(staticBgUrl);
+ token.ThrowIfCancellationRequested();
- bool overlayRequired = !string.IsNullOrEmpty(overlayUrl);
- bool staticRequired = !string.IsNullOrEmpty(staticBgUrl);
+ if (!string.IsNullOrEmpty(CurrentCachedBgKey))
+ {
+ LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-Background", context.OriginBackgroundImagePath ?? "");
+ LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-StaticBackground", context.OriginPlaceholderImagePath ?? "");
+ LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-Overlay", context.OriginOverlayImagePath ?? "");
+ }
- if (localBackground != null &&
- (!overlayRequired || localOverlay != null) &&
- (!staticRequired || localStatic != null))
- {
- bool willDisplayStatic = (!IsVideoMediaFileExtensionSupported(primaryBg ?? staticBgUrl!) ||
- (!CurrentIsEnableCustomImage &&
- !GlobalIsEnableCustomImage &&
- !CurrentIsEnableBackgroundAutoPlay))
- && localStatic != null;
- Uri accentPreviewUri = willDisplayStatic ? new Uri(localStatic!) : new Uri(localBackground);
-
- _ = RestoreSavedAccent(cachedBgKey, accentPreviewUri);
-
- ImageContextSources.Clear();
- ImageContextSources.Add(context);
- OnPropertyChanged(nameof(CurrentSelectedBackgroundContext));
- OnPropertyChanged(nameof(CurrentBackgroundCount));
-
- CurrentBackgroundElement = CreateLayerElement(localOverlay != null ? new Uri(localOverlay) : null,
- new Uri(localBackground),
- localStatic != null ? new Uri(localStatic) : null,
- context,
- IsVideoMediaFileExtensionSupported(primaryBg ?? staticBgUrl!));
-
- bool isUseFFmpeg = GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable;
- new Thread(async void (ctx) =>
+ // -- Use UI thread and load image layer
+ DispatcherQueueExtensions
+ .CurrentDispatcherQueue
+ .TryEnqueue(DispatcherQueuePriority.High,
+ AssignSource);
+
+ return;
+
+ async void AssignSource()
{
try
{
- await GetMediaAccentColor(ctx).ConfigureAwait(false);
+ string? backgroundToUse = (CurrentIsEnableBackgroundAutoPlay ? downloadedBackgroundUri : downloadedPlaceholderUri) ?? downloadedBackgroundUri;
+
+ // -- Avoid spawning element while window is currently not visible
+ // (for example: Minimized at start).
+ if (!WindowUtility.CurrentWindowIsVisible)
+ {
+ return;
+ }
+
+ // -- Avoid the same background to get loaded (unless Force reload is requested).
+ if (_lastBackgroundSource == backgroundToUse && !context.ForceReload)
+ {
+ return;
+ }
+
+ // -- Update last background source.
+ _lastBackgroundSource = backgroundToUse;
+
+ // -- Reset force reload request.
+ bool forceReloadRequested = context.ForceReload;
+ context.ForceReload = false;
+
+ // -- Check media support and whether to force FFmpeg in some cases.
+ (bool isSupported, bool forceFFmpeg) = await CheckCodecOrSpawnDialog(backgroundToUse);
+ if (!isSupported)
+ {
+ return;
+ }
+
+ // -- Notify whether the background is a video or not.
+ CurrentBackgroundIsVideo = context.IsVideo;
+
+ // -- Create or Get element and set background.
+ LayeredBackgroundImage element = CreateOrGetLayerElement();
+ bool useFFmpeg = forceFFmpeg || (context.IsVideo && GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable);
+
+ // -- If Force Reload previously requested, rapidly nullify the background source.
+ if (forceReloadRequested)
+ element.BackgroundSource = null;
+
+ element.UseFfmpegDecoder = useFFmpeg;
+ element.BackgroundSource = backgroundToUse;
+ element.ForegroundSource = downloadedOverlayUri;
+
+ // -- Load color accent
+ if (!string.IsNullOrEmpty(backgroundToUse))
+ LoadMediaAccentColor(new Uri(backgroundToUse), useFFmpeg);
}
catch (Exception e)
{
- Logger.LogWriteLine($"{e}", LogType.Error, true);
+ Console.WriteLine(e);
}
- })
- {
- IsBackground = true
- }.UnsafeStart((accentPreviewUri, isUseFFmpeg, cachedBgKey));
- return;
- }
- }
-
- // Show preset placeholder.
- string placeholderPath = GetPlaceholderBackgroundImageFrom(null);
- PresenterGrid.Background = new ImageBrush
- {
- ImageSource = new BitmapImage(new Uri(placeholderPath)),
- Stretch = Stretch.UniformToFill
- };
- CurrentBackgroundElement = null;
- _displayedContext = null;
- IsBackgroundLoading = true;
- }
-
- private async Task LoadImageAtIndexCore(int index, bool forceLoadToStatic, CancellationToken token, bool forceReload = false)
- {
- Stopwatch? stopwatch = null;
- try
- {
- bool isUseFFmpeg = GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable;
- stopwatch = Stopwatch.StartNew();
-
- // -- Notify changes on context menu properties
- DispatcherQueueExtensions
- .CurrentDispatcherQueue
- .TryEnqueue(() => OnPropertyChanged(nameof(CurrentSelectedBackgroundHasOverlayImage)));
-
- // -- Get context and invalidate previous CTS
- LayeredImageBackgroundContext context = ImageContextSources[index];
- if (Interlocked.Exchange(ref _imageLoadingTokenSource,
- CancellationTokenSource.CreateLinkedTokenSource(token)) is { } lastCts)
- {
- await lastCts.CancelAsync();
- lastCts.Dispose();
- }
-
- token = _imageLoadingTokenSource.Token;
-
- // -- Download overlay.
- Unsafe.SkipInit(out Uri? downloadedOverlayUri);
- if (Uri.TryCreate(context.OverlayImagePath, UriKind.Absolute, out Uri? overlayImageUri))
- {
- downloadedOverlayUri = await GetLocalOrDownloadedFilePath(overlayImageUri, token).ConfigureAwait(false);
- (downloadedOverlayUri, _) = await GetNativeOrDecodedImagePath(downloadedOverlayUri, token).ConfigureAwait(false);
- }
-
- // -- Download background.
- Unsafe.SkipInit(out Uri? downloadedBackgroundUri);
- if (Uri.TryCreate(context.BackgroundImagePath, UriKind.Absolute, out Uri? backgroundImageUri))
- {
- downloadedBackgroundUri = await GetLocalOrDownloadedFilePath(backgroundImageUri, token).ConfigureAwait(false);
- (downloadedBackgroundUri, _) = await GetNativeOrDecodedImagePath(downloadedBackgroundUri, token).ConfigureAwait(false);
- }
-
- // -- Download static background.
- Unsafe.SkipInit(out Uri? downloadedBackgroundStaticUri);
- if (Uri.TryCreate(context.BackgroundImageStaticPath, UriKind.Absolute, out Uri? backgroundStaticImageUri))
- {
- downloadedBackgroundStaticUri = await GetLocalOrDownloadedFilePath(backgroundStaticImageUri, token).ConfigureAwait(false);
- (downloadedBackgroundStaticUri, _) = await GetNativeOrDecodedImagePath(downloadedBackgroundStaticUri, token).ConfigureAwait(false);
- }
-
- // Try to use static bg URL if normal bg is not available.
- downloadedBackgroundUri ??= downloadedBackgroundStaticUri;
- if (downloadedBackgroundUri == null)
- {
- if (!context.IsCustom)
- {
- NeedFallbackPlaceholder = false;
- OnBackgroundLoadFailed?.Invoke();
}
- return;
}
-
- // -- Get upscaled image file if Waifu2X is enabled
- if (GlobalIsWaifu2XEnabled)
+ catch (Exception ex) when (ex is not OperationCanceledException)
{
- downloadedOverlayUri = await TryGetScaledWaifu2XImagePath(downloadedOverlayUri, token).ConfigureAwait(false);
- downloadedBackgroundUri = await TryGetScaledWaifu2XImagePath(downloadedBackgroundUri, token).ConfigureAwait(false);
- downloadedBackgroundStaticUri = await TryGetScaledWaifu2XImagePath(downloadedBackgroundStaticUri, token).ConfigureAwait(false);
+ _ = SentryHelper.ExceptionHandlerAsync(ex);
+ Logger.LogWriteLine($"[ImageBackgroundManager::LoadImage] {ex}",
+ LogType.Error,
+ true);
}
-
- token.ThrowIfCancellationRequested();
-
- // -- Check for codec support (Also spawn dialog to install either native WIC/MediaFoundation decoder or using Ffmpeg decoder)
- (bool isSupported, bool isVideo) = await CheckCodecOrSpawnDialog(downloadedBackgroundUri);
- if (!isSupported)
+ catch (OperationCanceledException)
{
- return;
+ // Suppress: cancellation is expected on game switch or rapid load cycles.
}
-
- // Try to force loading static image if requested.
- if (forceLoadToStatic && downloadedBackgroundStaticUri != null)
+ finally
{
- isVideo = false;
- downloadedBackgroundUri = downloadedBackgroundStaticUri;
- downloadedBackgroundStaticUri = null;
- }
-
- // Read accent color from the source that is actually displayed.
- bool willDisplayStatic = !isVideo ||
- (!CurrentIsEnableCustomImage &&
- !GlobalIsEnableCustomImage &&
- !CurrentIsEnableBackgroundAutoPlay);
- Uri? accentSourceUri = willDisplayStatic
- ? downloadedBackgroundStaticUri ?? downloadedBackgroundUri
- : downloadedBackgroundUri;
+ stopwatch?.Stop();
+ Logger.LogWriteLine($"Background image loading took: {stopwatch?.Elapsed.TotalSeconds} second(s)");
- new Thread(async void (ctx) =>
- {
- try
- {
- await GetMediaAccentColor(ctx).ConfigureAwait(false);
- }
- catch (Exception e)
- {
- Logger.LogWriteLine($"{e}", LogType.Error, true);
- }
- })
- {
- IsBackground = true
- }.UnsafeStart((accentSourceUri, isUseFFmpeg, CurrentCachedBgKey));
-
- if (!context.IsCustom &&
- !string.IsNullOrEmpty(CurrentCachedBgKey))
- {
- LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-Background", context.OriginBackgroundImagePath ?? "");
- LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-StaticBackground", context.OriginBackgroundStaticImagePath ?? "");
- LauncherConfig.SetAndSaveConfigValue($"{CurrentCachedBgKey}-Overlay", context.OriginOverlayImagePath ?? "");
- }
-
- // -- Use UI thread and load image layer
- int captureGen = _loadGeneration;
- bool captureForceReload = forceReload;
- DispatcherQueueExtensions
- .CurrentDispatcherQueue
- .TryEnqueue(() => SpawnImageLayer(downloadedOverlayUri,
- downloadedBackgroundUri,
- downloadedBackgroundStaticUri,
- isVideo,
- context,
- captureGen,
- forceReload: captureForceReload));
-
- GlobalIsFFmpegCurrentlyUsed = isVideo && isUseFFmpeg;
- }
- catch (Exception ex) when (ex is not OperationCanceledException)
- {
- _ = SentryHelper.ExceptionHandlerAsync(ex);
- Logger.LogWriteLine($"[ImageBackgroundManager::LoadImageAtIndex] {ex}",
- LogType.Error,
- true);
- }
- catch (OperationCanceledException)
- {
- // Suppress: cancellation is expected on game switch or rapid load cycles.
- }
- finally
- {
- stopwatch?.Stop();
- Logger.LogWriteLine($"Background image loading took: {stopwatch?.Elapsed.TotalSeconds} second(s)");
- if (!token.IsCancellationRequested)
- {
- IsBackgroundLoading = false;
+ DispatcherQueueExtensions.CurrentDispatcherQueue.TryEnqueue(() => IsBackgroundLoading = false);
}
}
}
- private void SpawnImageLayer(Uri? overlayFilePath,
- Uri? backgroundFilePath,
- Uri? backgroundStaticFilePath,
- bool isVideo,
- LayeredImageBackgroundContext context,
- int loadGeneration,
- bool forceReload = false)
+ private CancellationToken InvalidateAndRenewCancelToken(CancellationToken coopToken = default)
{
- if (loadGeneration != _loadGeneration)
- {
- return;
- }
+ CancellationTokenSource newCts;
+ CancellationTokenSource? lastCts =
+ Interlocked.Exchange(ref _imageLoadingTokenSource,
+ newCts = CancellationTokenSource.CreateLinkedTokenSource(coopToken));
- if (!forceReload &&
- CurrentBackgroundElement != null &&
- _displayedContext != null)
- {
- if (context.Equals(_displayedContext))
- {
- return;
- }
+ lastCts?.Cancel();
+ lastCts?.Dispose();
- if (CurrentBackgroundElement is LayeredBackgroundImage existingLayer &&
- IsSameLocalFile(existingLayer.BackgroundSource, backgroundFilePath) &&
- IsSameLocalFile(existingLayer.BackgroundStaticSource, backgroundStaticFilePath))
- {
- return;
- }
- }
-
- CurrentBackgroundElement = CreateLayerElement(overlayFilePath, backgroundFilePath, backgroundStaticFilePath, context, isVideo);
- }
-
- private static bool IsSameLocalFile(object? currentSource, Uri? newFilePath)
- {
- if (newFilePath == null) return currentSource == null;
- string? newPath = newFilePath.IsFile ? newFilePath.LocalPath : newFilePath.OriginalString;
- string? currentPath = currentSource switch
- {
- Uri uri => uri.IsFile ? uri.LocalPath : uri.OriginalString,
- string str => str,
- _ => null
- };
- return string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase);
+ return newCts.Token;
}
- private async Task RestoreSavedAccent(string cachedBgKey, Uri? fallbackSourceUri = null)
+ private LayeredBackgroundImage CreateOrGetLayerElement()
{
- string? savedHex = LauncherConfig.GetAppConfigValue($"{cachedBgKey}-AccentColor").ToString();
- if (!string.IsNullOrEmpty(savedHex) && savedHex!.Length >= 6 && ThemeRootElement != null)
+ if (CurrentBackgroundElement == null)
{
- if (TryParseHexColor(savedHex, out Color accentColor))
+ CurrentBackgroundElement = new LayeredBackgroundImage
{
- ThemeRootElement.ChangeAccentColor(accentColor);
- return;
- }
+ ParallaxResetOnUnfocused = false,
+ BackgroundElevationPixels = 64d
+ };
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.FfmpegDecoderModeProperty,
+ this,
+ nameof(GlobalFFmpegDecodingMode),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.ParallaxHoverSourceProperty,
+ this,
+ nameof(GlobalParallaxHoverSource),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.IsParallaxEnabledProperty,
+ this,
+ nameof(GlobalIsBackgroundParallaxEffectEnabled),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.ParallaxHorizontalShiftProperty,
+ this,
+ nameof(GlobalBackgroundParallaxPixelShift),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.ParallaxVerticalShiftProperty,
+ this,
+ nameof(GlobalBackgroundParallaxPixelShift),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.IsAudioEnabledProperty,
+ this,
+ nameof(GlobalBackgroundAudioEnabled),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.AudioVolumeProperty,
+ this,
+ nameof(GlobalBackgroundAudioVolume),
+ bindingMode: BindingMode.OneWay,
+ converter: MinMaxToPercentConverter);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.IsBackgroundElevatedProperty,
+ this,
+ nameof(IsBackgroundElevated),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.ForegroundOpacityProperty,
+ this,
+ nameof(ForegroundOpacity),
+ bindingMode: BindingMode.OneWay);
+
+ CurrentBackgroundElement.BindProperty(LayeredBackgroundImage.SmokeOpacityProperty,
+ this,
+ nameof(SmokeOpacity),
+ bindingMode: BindingMode.OneWay);
+
+ PresenterGrid?.Children.Clear();
+ PresenterGrid?.Children.Add(CurrentBackgroundElement);
}
- if (fallbackSourceUri != null && ThemeRootElement != null)
- {
- try
- {
- Color syncColor = await ColorPaletteUtility.GetMediaAccentColorFromAsync(fallbackSourceUri, false);
- ThemeRootElement.ChangeAccentColor(syncColor);
- LauncherConfig.SetAndSaveConfigValue($"{cachedBgKey}-AccentColor",
- $"{syncColor.R:X2}{syncColor.G:X2}{syncColor.B:X2}");
- }
- catch (Exception ex)
- {
- Logger.LogWriteLine($"RestoreSavedAccent failed: {ex}", LogType.Error, true);
- }
- }
- }
-
- private static bool TryParseHexColor(string hex, out Color color)
- {
- color = default;
- if (hex.Length < 6) return false;
- if (hex[0] == '#')
- hex = hex[1..];
- if (!uint.TryParse(hex, NumberStyles.HexNumber, null, out uint argb)) return false;
- if (hex.Length <= 6)
- argb = 0xFF_000000 | argb;
- color = Color.FromArgb((byte)(argb >> 24), (byte)(argb >> 16), (byte)(argb >> 8), (byte)argb);
- return true;
+ return CurrentBackgroundElement;
}
- private LayeredBackgroundImage CreateLayerElement(Uri? overlayFilePath,
- Uri? backgroundFilePath,
- Uri? backgroundStaticFilePath,
- LayeredImageBackgroundContext context,
- bool isVideo)
- {
- LayeredBackgroundImage layerElement = new()
- {
- BackgroundSource = backgroundFilePath,
- BackgroundStaticSource = backgroundStaticFilePath,
- ForegroundSource = overlayFilePath,
- UseFfmpegDecoder = GlobalIsUseFFmpeg && GlobalIsFFmpegAvailable,
- Tag = context,
- ParallaxResetOnUnfocused = false,
- BackgroundElevationPixels = 64d
- };
-
- if (!CurrentIsEnableCustomImage &&
- !GlobalIsEnableCustomImage)
- {
- layerElement.BindProperty(LayeredBackgroundImage.IsVideoAutoplayProperty,
- this,
- nameof(CurrentIsEnableBackgroundAutoPlay),
- bindingMode: BindingMode.OneWay,
- converter: StaticConverter.Shared);
- }
- else
- {
- layerElement.IsVideoAutoplay = WindowUtility.CurrentWindowIsVisible;
- }
-
- layerElement.BindProperty(LayeredBackgroundImage.FfmpegDecoderModeProperty,
- this,
- nameof(GlobalFFmpegDecodingMode),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.ParallaxHoverSourceProperty,
- this,
- nameof(GlobalParallaxHoverSource),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.IsParallaxEnabledProperty,
- this,
- nameof(GlobalIsBackgroundParallaxEffectEnabled),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.ParallaxHorizontalShiftProperty,
- this,
- nameof(GlobalBackgroundParallaxPixelShift),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.ParallaxVerticalShiftProperty,
- this,
- nameof(GlobalBackgroundParallaxPixelShift),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.IsAudioEnabledProperty,
- this,
- nameof(GlobalBackgroundAudioEnabled),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.AudioVolumeProperty,
- this,
- nameof(GlobalBackgroundAudioVolume),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.IsBackgroundElevatedProperty,
- this,
- nameof(IsBackgroundElevated),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.ForegroundOpacityProperty,
- this,
- nameof(ForegroundOpacity),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.SmokeOpacityProperty,
- this,
- nameof(SmokeOpacity),
- bindingMode: BindingMode.OneWay);
-
- layerElement.BindProperty(LayeredBackgroundImage.UseImageCacheProperty,
- context,
- nameof(context.ForceReload),
- bindingMode: BindingMode.OneWay,
- converter: StaticConverter.Shared);
-
- layerElement.ImageLoaded += LayerElementOnLoaded;
- PresenterGrid?.Children.Add(layerElement);
-
- layerElement.Tag = isVideo;
- _displayedContext = context;
- return layerElement;
- }
-
- private void LayerElementOnLoaded(LayeredBackgroundImage layerElement)
- {
- layerElement.ImageLoaded -= LayerElementOnLoaded;
- layerElement.Transitions.Add(new PopupThemeTransition());
-
- if (PresenterGrid != null)
- {
- List toRemove = PresenterGrid.Children.Where(element => element != layerElement).ToList();
- foreach (UIElement oldElement in toRemove)
- {
- if (oldElement is LayeredBackgroundImage oldLayer)
- {
- oldLayer.ImageLoaded -= LayerElementOnLoaded;
-
- if (oldLayer.Tag == null)
- {
- oldLayer.Opacity = 0;
- }
- else
- {
- PresenterGrid.Children.Remove(oldElement);
- }
- }
- else
- {
- PresenterGrid.Children.Remove(oldElement);
- }
- }
-
- PresenterGrid.Background = null;
- }
-
- if (CurrentIsEnableBackgroundAutoPlay && WindowUtility.CurrentWindowIsVisible)
- {
- Play(false);
- }
-
- CurrentBackgroundIsVideo = CurrentSelectedBackgroundContext?.IsVideo ?? false;
- if (layerElement.Tag is bool isDisplayControl)
- {
- CurrentBackgroundIsSeekable = isDisplayControl;
- }
- }
-
- private static bool TryGetUpscaledFilePath(string inputFilePath,
- out string upscaledFilePath)
- {
- upscaledFilePath = Path.Combine(LauncherConfig.AppGameImgCachedFolder, $"scaled_{Path.GetFileNameWithoutExtension(inputFilePath)}.png");
- if (LayeredBackgroundImage.SupportedVideoExtensionsLookup.Contains(Path.GetExtension(inputFilePath))) // Ignore if file is a video.
- {
- upscaledFilePath = inputFilePath;
- return true;
- }
-
- FileInfo outputFileInfo = new(upscaledFilePath);
- return outputFileInfo is { Exists: true, Length: > 0 };
- }
-
- private static async Task TryGetScaledWaifu2XImagePath(Uri? uri, CancellationToken token)
- {
- if (uri == null)
- {
- return null;
- }
-
- if (!uri.IsFile)
- {
- throw new InvalidOperationException("You must provide a local file path!");
- }
-
- // Ignore if the input file is a video
- string inputFilePath = uri.LocalPath;
- if (TryGetUpscaledFilePath(inputFilePath, out string outputFilePath))
- {
- return new Uri(outputFilePath);
- }
-
- FileInfo outputFileInfo = new(outputFilePath);
- outputFileInfo.Directory?.Create();
-
- await Task.Run(Impl, token);
- outputFileInfo.Refresh();
-
- try
- {
- token.ThrowIfCancellationRequested();
- return new Uri(outputFileInfo.FullName);
- }
- catch
- {
- outputFileInfo.TryDeleteFile();
- throw;
- }
-
- void Impl()
- {
- using ProcessingPipeline pipeline =
- MagicImageProcessor
- .BuildPipeline(inputFilePath,
- ProcessImageSettings.Default);
-
- using FileStream outputFileStream = outputFileInfo.Open(FileMode.Create,
- FileAccess.Write,
- FileShare.ReadWrite);
-
- pipeline.AddTransform(new Waifu2XTransform(ImageLoaderHelper.Waifu2X));
- pipeline.WriteOutput(outputFileStream);
- }
- }
-
- private async Task GetMediaAccentColor(object? context)
+ private async void LoadMediaAccentColor(Uri mediaUri, bool useFFmpeg)
{
try
{
- if (context is not (Uri asUri, bool useFfmpegForVideo, string configKey))
- {
- return;
- }
-
- Color color = await ColorPaletteUtility.GetMediaAccentColorFromAsync(asUri, useFfmpegForVideo)
+ Color color = await ColorPaletteUtility.GetMediaAccentColorFromAsync(mediaUri, useFFmpeg)
.ConfigureAwait(false);
- if (color == default(Color)) return;
-
- string hex = $"{color.R:X2}{color.G:X2}{color.B:X2}";
- if (!string.IsNullOrEmpty(configKey))
- {
- string? savedHex = LauncherConfig.GetAppConfigValue($"{configKey}-AccentColor").ToString();
- if (savedHex == hex) return;
- LauncherConfig.SetAndSaveConfigValue($"{configKey}-AccentColor", hex);
- }
-
ColorAccentChanged?.Invoke(color);
}
catch (Exception ex)
{
- Logger.LogWriteLine($"GetMediaAccentColor failed: {ex}", LogType.Error, true);
+ Logger.LogWriteLine($"LoadMediaAccentColor failed: {ex}", LogType.Error, true);
SentryHelper.ExceptionHandler(ex);
}
}
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.StreamAndPath.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.StreamAndPath.cs
index ec50a11353..1af6185487 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.StreamAndPath.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.StreamAndPath.cs
@@ -1,5 +1,6 @@
using CollapseLauncher.Helper.Metadata;
using CollapseLauncher.Helper.StreamUtility;
+using Hi3Helper.Data;
using Hi3Helper.EncTool;
using Hi3Helper.Shared.Region;
using System;
@@ -21,38 +22,47 @@ public partial class ImageBackgroundManager
#region Fields
private static readonly string[] PlaceholderBackgroundImageRelPath = [
- @"Assets\Images\PageBackground\default_genshin.webp",
- @"Assets\Images\PageBackground\default_honkai.webp",
- @"Assets\Images\PageBackground\default_starrail.webp",
- @"Assets\Images\PageBackground\default_zzz.webp",
- ];
-
- private static readonly string[] PlaceholderBackgroundImagePngRelPath = [
- @"Assets\Images\GamePoster\poster_genshin.png",
- @"Assets\Images\GamePoster\poster_honkai.png",
- @"Assets\Images\GamePoster\poster_starrail.png",
- @"Assets\Images\GamePoster\poster_zzz.png",
+ @"Assets\Images\PageBackground\default_genshin.avif",
+ @"Assets\Images\PageBackground\default_honkai.avif",
+ @"Assets\Images\PageBackground\default_starrail.avif",
+ @"Assets\Images\PageBackground\default_zzz.avif"
];
#endregion
- private static Task OpenStreamFromFileOrUrl(string? filePath, CancellationToken token)
+ public static ValueTask OpenStreamFromFileOrUrl(string? filePath, CancellationToken token)
{
- return !Uri.TryCreate(filePath, UriKind.Absolute, out Uri? uri)
+ return !Uri.TryCreate(filePath, UriKind.RelativeOrAbsolute, out Uri? uri)
? throw new InvalidOperationException($"File path or URL is misformed! {filePath}")
: OpenStreamFromFileOrUrl(uri, token);
}
- private static async Task OpenStreamFromFileOrUrl(Uri uri, CancellationToken token)
+ public static async ValueTask OpenStreamFromFileOrUrl(Uri uri, CancellationToken token)
{
+ if (!uri.IsAbsoluteUri && !uri.IsWellFormedOriginalString())
+ {
+ string localPath = uri.OriginalString.GetFullyQualifiedPath();
+ return !File.Exists(localPath)
+ ? throw new FileNotFoundException($"File: {localPath} does not exist!")
+ : File.Open(localPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ }
+
if (uri.IsFile)
{
- string localPath = uri.LocalPath;
+ string localPath = uri.LocalPath.GetFullyQualifiedPath();
return !File.Exists(localPath)
? throw new FileNotFoundException($"File: {localPath} does not exist!")
: File.Open(localPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
}
+ if (uri.Scheme.StartsWith("ms-appx"))
+ {
+ string uwpResPath = uri.LocalPath.TrimStart("\\/").GetFullyQualifiedPath();
+ return !File.Exists(uwpResPath)
+ ? throw new FileNotFoundException($"File: {uwpResPath} does not exist!")
+ : File.Open(uwpResPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+ }
+
// Find stamp file. Try read the metadata in it and just use locally cached files if matches.
if (TryGetDownloadedFile(uri,
out FileInfo downloadedFilePath,
@@ -63,14 +73,9 @@ private static async Task OpenStreamFromFileOrUrl(Uri uri, Cancellat
}
HttpClient sharedClient = FallbackCDNUtil.GetGlobalHttpClient(true);
- status = await sharedClient.GetCachedUrlStatus(uri, token);
+ status = await sharedClient.GetCachedUrlStatus(uri, token).ConfigureAwait(false);
status.EnsureSuccessStatusCode();
- if (status.FileSize == 0)
- {
- throw new HttpRequestException($"File URL is reachable but it returns 0 bytes. {uri}");
- }
-
downloadedFilePath = downloadedFilePath
.EnsureCreationOfDirectory()
.EnsureNoReadOnly();
@@ -82,9 +87,14 @@ private static async Task OpenStreamFromFileOrUrl(Uri uri, Cancellat
downloadedFilePath.Open(FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite);
using HttpResponseMessage responseMessage =
- await sharedClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, token);
- await using Stream responseStream = await responseMessage.Content.ReadAsStreamAsync(token);
- await responseStream.CopyToAsync(downloadedFileStream, token);
+ await sharedClient.GetAsync(uri, HttpCompletionOption.ResponseHeadersRead, token)
+ .ConfigureAwait(false);
+ await using Stream responseStream =
+ await responseMessage.Content
+ .ReadAsStreamAsync(token)
+ .ConfigureAwait(false);
+ await responseStream.CopyToAsync(downloadedFileStream, token)
+ .ConfigureAwait(false);
// Write stamp for future cache metadata
ReadOnlySpan stampData = AsSpan(in status);
@@ -97,13 +107,13 @@ static unsafe ReadOnlySpan AsSpan(in T data)
where T : unmanaged => new(Unsafe.AsPointer(in data), sizeof(T));
}
- internal static async Task GetLocalOrDownloadedFilePath(Uri uri, CancellationToken token)
+ internal static async ValueTask GetLocalOrDownloadedFilePath(Uri uri, CancellationToken token)
{
await using FileStream stream = await OpenStreamFromFileOrUrl(uri, token);
return new Uri(stream.Name);
}
- internal static async Task GetLocalOrDownloadedFilePath(string path, CancellationToken token)
+ internal static async ValueTask GetLocalOrDownloadedFilePath(string path, CancellationToken token)
{
Uri uri = await GetLocalOrDownloadedFilePath(new Uri(path), token);
return uri.IsFile
@@ -111,7 +121,7 @@ internal static async Task GetLocalOrDownloadedFilePath(string path, Can
: uri.ToString();
}
- private static bool TryGetCroppedImageFilePath(string filePath,
+ internal static bool TryGetCroppedImageFilePath(string filePath,
out string croppedFilePath)
{
string imageDirPath = LauncherConfig.AppGameImgFolder;
@@ -121,8 +131,8 @@ private static bool TryGetCroppedImageFilePath(string filePath,
return IsFileExistAndNotEmpty(croppedFilePath);
}
- private static bool TryGetDecodedTemporaryFile(string filePath,
- out string decodedFilePath)
+ internal static bool TryGetDecodedTemporaryFile(string filePath,
+ out string decodedFilePath)
{
string imageDirPath = LauncherConfig.AppGameImgFolder;
string fileName = $"decoded_{Path.GetFileNameWithoutExtension(filePath)}.png";
@@ -137,10 +147,10 @@ private static bool IsFileExistAndNotEmpty(string filePath)
return fileInfo is { Exists: true, Length: > 0 };
}
- private static bool TryGetDownloadedFile(Uri filePath,
- out FileInfo downloadedFilePath,
- out FileInfo downloadStampFilePath,
- out UrlStatus urlStatusStamp)
+ internal static bool TryGetDownloadedFile(Uri filePath,
+ out FileInfo downloadedFilePath,
+ out FileInfo downloadStampFilePath,
+ out UrlStatus urlStatusStamp)
{
string imageDirPath = LauncherConfig.AppGameImgFolder;
string fileHashedName = $"{Path.GetFileName(filePath.AbsolutePath)}";
@@ -180,68 +190,24 @@ private static unsafe bool TryReadDownloadStampFile(
return true;
}
- internal static string GetPlaceholderBackgroundImageFrom(PresetConfig? presetConfig, bool usePngVersion = false)
+ internal static string GetPlaceholderBackgroundImageFrom(PresetConfig? presetConfig)
{
- string[] source = usePngVersion
- ? PlaceholderBackgroundImagePngRelPath
- : PlaceholderBackgroundImageRelPath;
-
- string relPath = presetConfig?.GameName ??
- LauncherConfig.GetAppConfigValue("GameCategory").Value switch
+ string[] source = PlaceholderBackgroundImageRelPath;
+ string relPath = presetConfig?.GameName switch
{
"Genshin Impact" => source[0],
"Honkai Impact 3rd" => source[1],
"Honkai: Star Rail" => source[2],
"Zenless Zone Zero" => source[3],
- _ => GetRandomPlaceholderImage(usePngVersion)
+ _ => GetRandomPlaceholderImage()
};
return Path.Combine(LauncherConfig.AppExecutableDir, relPath);
}
- public static string GetRandomPlaceholderImage(bool usePngVersion = false)
- {
- string[] source = usePngVersion
- ? PlaceholderBackgroundImagePngRelPath
- : PlaceholderBackgroundImageRelPath;
-
- return source[Random.Shared.Next(0, source.Length - 1)];
- }
-
- internal static bool CheckIsContextCached(LayeredImageBackgroundContext ctx)
- {
- bool backgroundCached = false;
- if (!string.IsNullOrEmpty(ctx.BackgroundImagePath) &&
- TryGetDownloadedFile(new Uri(ctx.BackgroundImagePath), out _, out _, out _))
- {
- backgroundCached = true;
- }
- else if (!string.IsNullOrEmpty(ctx.BackgroundImageStaticPath) &&
- TryGetDownloadedFile(new Uri(ctx.BackgroundImageStaticPath), out _, out _, out _))
- {
- backgroundCached = true;
- }
-
- return backgroundCached;
- }
-
- internal static string? TryGetCachedLocalPath(string? urlOrPath)
+ public static string GetRandomPlaceholderImage()
{
- if (string.IsNullOrEmpty(urlOrPath))
- {
- return null;
- }
-
- if (TryGetDecodedTemporaryFile(urlOrPath, out string decodedFilePath))
- {
- return decodedFilePath;
- }
-
- if (TryGetDownloadedFile(new Uri(urlOrPath), out FileInfo downloadedFile, out _, out _))
- {
- return downloadedFile.FullName;
- }
-
- return null;
+ string[] source = PlaceholderBackgroundImageRelPath;
+ return source[new Random(Environment.TickCount).Next(0, source.Length - 1)];
}
}
diff --git a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.cs b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.cs
index 0af623e028..b229bca895 100644
--- a/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.cs
+++ b/CollapseLauncher/Classes/GameManagement/ImageBackground/ImageBackgroundManager.cs
@@ -2,6 +2,7 @@
using CollapseLauncher.Helper.Image;
using CollapseLauncher.Helper.LauncherApiLoader.HoYoPlay;
using CollapseLauncher.Helper.Metadata;
+using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.Interfaces;
using CollapseLauncher.Interfaces.Class;
using CollapseLauncher.XAMLs.Theme.CustomControls;
@@ -13,12 +14,14 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Windows.UI;
using WinRT;
+// ReSharper disable NonReadonlyMemberInGetHashCode
// ReSharper disable CheckNamespace
#pragma warning disable IDE0130
@@ -199,8 +202,6 @@ public double SmokeOpacity
private Grid? PresenterGrid { get; set; }
private PresetConfig? PresetConfig { get; set; }
- internal bool NeedFallbackPlaceholder { get; set; }
-
public Action? OnBackgroundLoadFailed { get; set; }
public string? CurrentCustomBackgroundImagePath
@@ -257,7 +258,7 @@ public int CurrentSelectedBackgroundIndex
LauncherConfig.SetAndSaveConfigValue(CurrentSelectedBackgroundIndexKey, value);
OnPropertyChanged();
- LoadImageAtIndex(value, false, CancellationToken.None, true);
+ LoadImage();
}
}
@@ -287,16 +288,6 @@ private set
public int CurrentBackgroundCount => ImageContextSources.Count;
- public bool CurrentBackgroundIsSeekable
- {
- get;
- private set
- {
- field = value;
- OnPropertyChanged();
- }
- }
-
public bool CurrentBackgroundIsVideo
{
get;
@@ -321,10 +312,6 @@ public ObservableCollection ImageContextSources
private int _previousContextHash;
- private volatile int _loadGeneration;
-
- private LayeredImageBackgroundContext? _displayedContext;
-
internal FrameworkElement? ThemeRootElement { get; set; }
#endregion
@@ -383,6 +370,7 @@ async void Impl()
// -- Try to initialize custom image first
// -- A. Check for per-region custom background
if (CurrentIsEnableCustomImage &&
+ !string.IsNullOrEmpty(CurrentCustomBackgroundImagePath) &&
await SetCurrentCustomBackground(CurrentCustomBackgroundImagePath, false, false, token))
{
return;
@@ -391,6 +379,7 @@ await SetCurrentCustomBackground(CurrentCustomBackgroundImagePath, false, false,
// -- B. Check for global custom background
if (!CurrentIsEnableCustomImage &&
GlobalIsEnableCustomImage &&
+ !string.IsNullOrEmpty(GlobalCustomBackgroundImagePath) &&
await SetGlobalCustomBackground(GlobalCustomBackgroundImagePath, false, false, token))
{
return;
@@ -408,8 +397,6 @@ await SetGlobalCustomBackground(GlobalCustomBackgroundImagePath, false, false, t
imageContexts.Add(new LayeredImageBackgroundContext
{
OriginBackgroundImagePath = bgPlaceholderPath,
- BackgroundImagePath = bgPlaceholderPath,
- IsVideo = IsVideoMediaFileExtensionSupported(bgPlaceholderPath),
IsCustom = true
});
return;
@@ -419,20 +406,15 @@ await SetGlobalCustomBackground(GlobalCustomBackgroundImagePath, false, false, t
// ReSharper disable once LoopCanBeConvertedToQuery
foreach (HypLauncherBackgroundContentKindData contextEntry in contextList.SelectMany(x => x.Backgrounds))
{
- string? overlayImagePath = contextEntry.BackgroundOverlay?.ImageUrl;
- string? backgroundImagePath = contextEntry.BackgroundVideo?.ImageUrl ??
- contextEntry.BackgroundImage?.ImageUrl;
- string? backgroundImageStaticPath = contextEntry.BackgroundImage?.ImageUrl;
+ string? overlayImagePath = contextEntry.BackgroundOverlay?.ImageUrl;
+ string? backgroundImagePath = contextEntry.BackgroundVideo?.ImageUrl ?? contextEntry.BackgroundImage?.ImageUrl;
+ string? placeholderImagePath = contextEntry.BackgroundImage?.ImageUrl;
imageContexts.Add(new LayeredImageBackgroundContext
{
- OriginOverlayImagePath = overlayImagePath,
- OriginBackgroundImagePath = backgroundImagePath,
- OriginBackgroundStaticImagePath = backgroundImageStaticPath,
- OverlayImagePath = overlayImagePath,
- BackgroundImagePath = backgroundImagePath,
- BackgroundImageStaticPath = backgroundImageStaticPath,
- IsVideo = IsVideoMediaFileExtensionSupported(backgroundImagePath)
+ OriginOverlayImagePath = overlayImagePath,
+ OriginBackgroundImagePath = backgroundImagePath,
+ OriginPlaceholderImagePath = placeholderImagePath
});
}
}
@@ -485,14 +467,13 @@ private async Task SetCustomBackgroundCore(string? imagePath,
}
// -- Try to perform cropped or pass through processing
- (_, string? resultBackgroundPath, bool isCancelProcess) =
- await GetCroppedCustomImage(null,
- imagePath,
- performCropRequest,
- token);
+ GetCroppedResult cropResult = await GetCroppedCustomImage(null,
+ imagePath,
+ performCropRequest,
+ token);
// -- Do not process custom background if cancelled.
- if (isCancelProcess)
+ if (cropResult.IsCancelResize)
{
return false;
}
@@ -507,11 +488,8 @@ await GetCroppedCustomImage(null,
// -- Apply background
LayeredImageBackgroundContext context = new()
{
- OriginBackgroundImagePath = imagePath,
- BackgroundImagePath = resultBackgroundPath,
- ForceReload = true, // Request force reload (skip cache)
- IsVideo = IsVideoMediaFileExtensionSupported(resultBackgroundPath),
- IsCustom = true
+ OriginBackgroundImagePath = cropResult.ProcessedBackgroundPath,
+ ForceReload = cropResult.WasCropPerformed // Only do force reload if crop was performed.
};
UpdateContextListCore(token, skipPreviousContextCheck, context);
@@ -538,33 +516,14 @@ private void UpdateContextListCore(
return;
}
- UpdateContextListCore(token, skipPreviousContextCheck, imageContexts.ToArray());
+ UpdateContextListCore(token, skipPreviousContextCheck, [.. imageContexts]);
}
private void UpdateContextListCore(
CancellationToken token,
- bool skipPreviousContextCheck,
+ bool forceReload,
params ReadOnlySpan imageContexts)
{
- UpdateContextListCore(token, skipPreviousContextCheck, false, imageContexts);
- }
-
- private void UpdateContextListCore(
- CancellationToken token,
- bool skipPreviousContextCheck,
- bool forceLoadToStatic,
- params ReadOnlySpan imageContexts)
- {
- // Do not update if the previous contexts are equal
- if ((!skipPreviousContextCheck &&
- IsContextEqual(imageContexts)) ||
- imageContexts.IsEmpty)
- {
- return;
- }
-
- Interlocked.Increment(ref _loadGeneration);
-
ImageContextSources.Clear(); // Flush list
ref IList? backedList =
ref ObservableCollectionExtension
@@ -591,32 +550,14 @@ ref ObservableCollectionExtension
OnPropertyChanged(nameof(CurrentSelectedBackgroundIndex));
OnPropertyChanged(nameof(CurrentSelectedBackgroundContext));
OnPropertyChanged(nameof(CurrentBackgroundCount));
- LoadImageAtIndex(CurrentSelectedBackgroundIndex, forceLoadToStatic, token);
+ LoadImage(token);
}
#pragma warning restore CA1068
- private bool IsContextEqual(ReadOnlySpan imageContexts)
- {
- if (ImageContextSources.Count != imageContexts.Length)
- {
- return false;
- }
-
- HashCode hashCode = default;
- foreach (LayeredImageBackgroundContext context in imageContexts)
- {
- hashCode.Add(context);
- }
-
- int hashCodeRes = hashCode.ToHashCode();
- return Interlocked.Exchange(ref _previousContextHash, hashCodeRes) == hashCodeRes;
- }
-
public void ResetContexts()
{
Interlocked.Exchange(ref _previousContextHash, 0);
ImageContextSources.Clear();
- NeedFallbackPlaceholder = false;
}
void INotifyAllPropertyChanged.NotifyAllChanged()
@@ -638,49 +579,79 @@ public string? OriginOverlayImagePath
public string? OriginBackgroundImagePath
{
get;
- init;
+ init
+ {
+ IsVideo = ImageBackgroundManager.IsVideoMediaFileExtensionSupported(value);
+ field = value;
+ }
}
- public string? OriginBackgroundStaticImagePath
+ public string? OriginPlaceholderImagePath
{
get;
init;
}
- public string? OverlayImagePath
+ public bool ForceReload
{
get;
- init;
+ set;
}
- public string? BackgroundImagePath
+ public bool IsVideo
{
get;
- init;
+ private init;
}
- public string? BackgroundImageStaticPath
+ public bool IsCustom
{
get;
init;
}
- public bool ForceReload
+ public async Task GetCachedImagePathAsync(string? path, CancellationToken token)
{
- get;
- init;
- }
+ if (path == null)
+ return null;
- public bool IsVideo
- {
- get;
- init;
+ path = await ImageBackgroundManager.GetLocalOrDownloadedFilePath(path, token).ConfigureAwait(false);
+ (path, _) = await ImageBackgroundManager.GetNativeOrDecodedImagePath(path, token).ConfigureAwait(false);
+
+ if (IsVideo || !ImageBackgroundManager.Shared.GlobalIsWaifu2XEnabled)
+ {
+ return path;
+ }
+
+ Uri upscaledUri = await ImageBackgroundManager.TryGetScaledWaifu2XImagePath(new Uri(path), token).ConfigureAwait(false);
+ return upscaledUri.LocalPath;
}
- public bool IsCustom
+ public void DeleteCachedImagePath(string? path)
{
- get;
- init;
+ if (string.IsNullOrEmpty(path))
+ return;
+
+ Uri uri = new(path);
+ if (ImageBackgroundManager.TryGetDownloadedFile(uri,
+ out FileInfo downloadedFilePath,
+ out _,
+ out _))
+ {
+ downloadedFilePath.TryDeleteFile();
+ }
+
+ if (ImageBackgroundManager.TryGetDecodedTemporaryFile(downloadedFilePath.FullName,
+ out string decodedFilePath))
+ {
+ StreamExtension.TryDeleteFile(decodedFilePath);
+ }
+
+ if (ImageBackgroundManager.TryGetUpscaledFilePath(decodedFilePath,
+ out string upscaledFilePath))
+ {
+ StreamExtension.TryDeleteFile(upscaledFilePath);
+ }
}
public bool Equals(LayeredImageBackgroundContext? other) => other?.GetHashCode() == GetHashCode();
@@ -699,10 +670,7 @@ public override bool Equals(object? obj)
public override int GetHashCode() =>
HashCode.Combine(OriginOverlayImagePath,
OriginBackgroundImagePath,
- OriginBackgroundStaticImagePath,
- OverlayImagePath,
- BackgroundImagePath,
- BackgroundImageStaticPath,
- ForceReload,
- IsVideo);
+ OriginPlaceholderImagePath,
+ IsVideo,
+ IsCustom);
}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/GameManagement/WpfPackage/WpfPackageContext.Methods.cs b/CollapseLauncher/Classes/GameManagement/WpfPackage/WpfPackageContext.Methods.cs
index 9d770bc49e..3743c09ae7 100644
--- a/CollapseLauncher/Classes/GameManagement/WpfPackage/WpfPackageContext.Methods.cs
+++ b/CollapseLauncher/Classes/GameManagement/WpfPackage/WpfPackageContext.Methods.cs
@@ -256,10 +256,7 @@ private async void SpawnUpdateFinishedNotification()
string gameNameTranslated = MetadataHelper.GetTranslatedTitle(gameName);
string gameRegionTranslated = MetadataHelper.GetTranslatedRegion(regionName);
- string icon = await ImageLoaderHelper
- .GetCachedSpritesAsync(WpfPackageIconUrl,
- false,
- CancellationToken.None)
+ string icon = await ImageLoaderHelper.GetCachedSpritesAsync(WpfPackageIconUrl, false, CancellationToken.None)
?? "";
// Create Toast Notification Content
diff --git a/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs b/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs
index a6f698043d..23d6a92975 100644
--- a/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs
+++ b/CollapseLauncher/Classes/Helper/Background/ColorPaletteUtility.cs
@@ -62,17 +62,16 @@ private static byte[] GetSharedBufferOrResizeTo(int size)
}
}
- public static async Task GetMediaAccentColorFromAsync(
+ public static ValueTask GetMediaAccentColorFromAsync(
Uri uri,
bool useFfmpegForVideo,
CancellationToken token = default)
{
// -- Ignore if file doesn't exist or not a file URI
string filePath = uri.LocalPath;
- if (!uri.IsFile ||
- !File.Exists(filePath))
+ if (!uri.IsFile || !File.Exists(filePath))
{
- return default;
+ return ValueTask.FromResult(Color.FromArgb(255, 255, 255, 255));
}
// -- Try to get cached palette data
@@ -80,47 +79,63 @@ public static async Task GetMediaAccentColorFromAsync(
out Color cachedColors,
out string cachedPaletteFile))
{
- return cachedColors;
+ return ValueTask.FromResult(cachedColors);
}
- // -- If not cached due to it not existing or has invalid format, generate new palette data
- FileInfo cachedPaletteFileInfo = new FileInfo(cachedPaletteFile)
- .EnsureCreationOfDirectory();
- cachedPaletteFileInfo.Directory?.Create();
-
- // -- Get source stream
- object? sourceStreamObj = await GetMediaFrameStream(filePath, token);
-
- return await Task.Factory.StartNew(Impl, TaskCreationOptions.DenyChildAttach);
-
- Color Impl()
+ TaskCompletionSource processTcs = new();
+ new Thread(Impl)
{
- using IRandomAccessStream? sourceRandomStream = sourceStreamObj as IRandomAccessStream;
- using Stream? sourceStream = sourceStreamObj as Stream ?? sourceRandomStream?.AsStream();
+ IsBackground = true,
+ Name = "Collapse Launcher Media Accent Color Picker"
+ }.UnsafeStart();
+ return new ValueTask(processTcs.Task);
- if (sourceStream == null)
+ async void Impl()
+ {
+ try
{
- return default;
- }
+ // -- If not cached due to it not existing or has invalid format, generate new palette data
+ FileInfo cachedPaletteFileInfo = new FileInfo(cachedPaletteFile).EnsureCreationOfDirectory();
+ cachedPaletteFileInfo.Directory?.Create();
- ImageFileInfo fileInfo = ImageFileInfo.Load(sourceStream);
- ImageFileInfo.FrameInfo frameInfo = fileInfo.Frames[0];
- sourceStream.Seek(0, SeekOrigin.Begin);
+ // -- Get source stream
+ object? sourceStreamObj = await GetMediaFrameStream(filePath, token).ConfigureAwait(false);
- // -- Process palette
- int channels = frameInfo.HasAlpha ? 4 : 3;
- byte[] tempBuffer = GetSharedBufferOrResizeTo(frameInfo.Width * frameInfo.Height * channels + 54);
- using MemoryStream tempStream = new(tempBuffer);
- ProcessImageSettings settings = new();
- settings.TrySetEncoderFormat(ImageMimeTypes.Bmp);
- MagicImageProcessor.ProcessImage(sourceStream, tempStream, settings);
+ using IRandomAccessStream? sourceRandomStream = sourceStreamObj as IRandomAccessStream;
+ await using Stream? sourceStream = sourceStreamObj as Stream ?? sourceRandomStream?.AsStream();
- Span tempDataSpan = tempBuffer.AsSpan(54, (int)tempStream.Position - 54);
- Color colorArray = GetPaletteFromSpan(tempDataSpan, frameInfo.Width, frameInfo.Height, channels);
- ReadOnlySpan colorSpanAsBytes = MemoryMarshal.AsBytes([colorArray]);
+ if (sourceStream == null)
+ {
+ processTcs.SetResult(Color.FromArgb(255, 255, 255, 255));
+ return;
+ }
- File.WriteAllBytes(cachedPaletteFile, colorSpanAsBytes);
- return colorArray;
+ processTcs.SetResult(await Task.Run(() =>
+ {
+ ImageFileInfo fileInfo = ImageFileInfo.Load(sourceStream);
+ ImageFileInfo.FrameInfo frameInfo = fileInfo.Frames[0];
+ sourceStream.Seek(0, SeekOrigin.Begin);
+
+ // -- Process palette
+ int channels = frameInfo.HasAlpha ? 4 : 3;
+ byte[] tempBuffer = GetSharedBufferOrResizeTo(frameInfo.Width * frameInfo.Height * channels + 54);
+ using MemoryStream tempStream = new(tempBuffer);
+ ProcessImageSettings settings = new();
+ settings.TrySetEncoderFormat(ImageMimeTypes.Bmp);
+ MagicImageProcessor.ProcessImage(sourceStream, tempStream, settings);
+
+ Span tempDataSpan = tempBuffer.AsSpan(54, (int)tempStream.Position - 54);
+ Color colorArray = GetPaletteFromSpan(tempDataSpan, frameInfo.Width, frameInfo.Height, channels);
+ ReadOnlySpan colorSpanAsBytes = MemoryMarshal.AsBytes([colorArray]);
+
+ File.WriteAllBytes(cachedPaletteFile, colorSpanAsBytes);
+ return colorArray;
+ }, token));
+ }
+ catch (Exception ex)
+ {
+ processTcs.SetException(ex);
+ }
}
}
@@ -336,7 +351,7 @@ static unsafe FileStream CreateSvgTempStream(int width, int height, Span b
}
static unsafe ref T AsRef(Span span)
- => ref Unsafe.AsRef(Unsafe.AsPointer(ref MemoryMarshal.AsRef(span)));
+ => ref Unsafe.As(ref MemoryMarshal.AsRef(span));
}
catch (Exception ex)
{
diff --git a/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaSettings.cs b/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaSettings.cs
new file mode 100644
index 0000000000..0244969673
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaSettings.cs
@@ -0,0 +1,32 @@
+// ReSharper disable IdentifierTypo
+#pragma warning disable IDE0130
+
+#nullable enable
+namespace CollapseLauncher.Helper.DaftarPustaka;
+
+public enum DaftarPustakaCompression : ushort
+{
+ Uncompressed,
+ Brotli,
+ Zstandard,
+ Deflate,
+ Zlib,
+ Lzma,
+ Lzma2,
+ BZip2
+}
+
+public enum DaftarPustakaEncryption : ushort
+{
+ Unencrypted,
+ Aes,
+ Rsa,
+ Xor
+}
+
+
+internal static class DaftarPustakaSettings
+{
+ internal const ulong CollapseHeader = 7310310183885631299u;
+ internal const ushort CurrentVersion = 3;
+}
diff --git a/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaUtil.Decompressor.cs b/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaUtil.Decompressor.cs
new file mode 100644
index 0000000000..972d2fe50b
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaUtil.Decompressor.cs
@@ -0,0 +1,96 @@
+// ReSharper disable IdentifierTypo
+using System;
+using System.IO;
+using System.IO.Compression;
+using CollapseLauncher.Helper.StreamUtility;
+
+#if NET11_0_OR_GREATER
+using ZstandardNativeStream = System.IO.Compression.ZstandardStream;
+#else
+using System.Collections.Generic;
+using ZstandardNativeStream = ZstdNet.DecompressionStream;
+using ZstandardNativeDecompressionParameter = ZstdNet.ZSTD_dParameter;
+using ZstandardNativeDecompressionOptions = ZstdNet.DecompressionOptions;
+#endif
+
+#if !NET11_0_OR_GREATER
+using ZstandardManagedDecompressorParameter = ZstdSharp.Unsafe.ZSTD_dParameter;
+using ZstandardManagedStream = ZstdSharp.DecompressionStream;
+using ZstdNet;
+#endif
+#pragma warning disable IDE0130
+
+#nullable enable
+namespace CollapseLauncher.Helper.DaftarPustaka;
+
+internal static partial class DaftarPustakaUtil
+{
+ public static class Decompressor
+ {
+ public static BrotliStream CreateBrotliStream(Stream source, bool leaveOpen) =>
+ new(source, CompressionMode.Decompress, leaveOpen);
+
+ public static Stream CreateZStandardStream(Stream source, bool leaveOpen)
+ {
+ int dictionaryBufferLength = source.Read();
+ int maxWindowLog2 = source.Read();
+ byte[]? dictionaryBuffer = null;
+
+#if NET11_0_OR_GREATER
+ int quality = 0;
+#endif
+
+ // Read customized dictionary settings.
+ if (dictionaryBufferLength > 0)
+ {
+#if NET11_0_OR_GREATER
+ quality = source.Read();
+#else
+ _ = source.Read();
+#endif
+ dictionaryBuffer = source.ReadArray(dictionaryBufferLength);
+ }
+
+#if NET11_0_OR_GREATER
+ ZstandardDictionary? dictionary = ZstandardDictionary.Create(dictionaryBuffer, quality);
+
+ return dictionary != null
+ ? new ZstandardStream(source, CompressionMode.Decompress, dictionary, leaveOpen)
+ : new ZstandardStream(source, new ZstandardDecompressionOptions
+ {
+ Dictionary = dictionary,
+ MaxWindowLog2 = maxWindowLog2
+ }, leaveOpen);
+#else
+ if (DllUtils.IsLibraryExist(DllUtils.DllName))
+ {
+ var parameters = new Dictionary
+ {
+ { ZstandardNativeDecompressionParameter.ZSTD_d_windowLogMax, maxWindowLog2 }
+ };
+ return new ZstandardNativeStream(source,
+ new ZstandardNativeDecompressionOptions(dictionaryBuffer, parameters),
+ leaveOpen: leaveOpen);
+ }
+
+ ZstandardManagedStream managedStream = new(source, leaveOpen: leaveOpen);
+ managedStream.SetParameter(ZstandardManagedDecompressorParameter.ZSTD_d_windowLogMax, maxWindowLog2);
+ if (dictionaryBuffer != null) managedStream.LoadDictionary(dictionaryBuffer);
+
+ return managedStream;
+#endif
+ }
+
+ public static DeflateStream CreateDeflateStream(Stream source, bool leaveOpen)
+ => new(source, CompressionMode.Decompress, leaveOpen);
+
+ public static ZLibStream CreateZlibStream(Stream source, bool leaveOpen)
+ => new(source, CompressionMode.Decompress, leaveOpen);
+
+ public static ZLibStream CreateLzmaStream(Stream source, bool leaveOpen)
+ => throw new NotImplementedException();
+
+ public static ZLibStream CreateBZip2Stream(Stream source, bool leaveOpen)
+ => throw new NotImplementedException();
+ }
+}
diff --git a/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaUtil.Decryptor.cs b/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaUtil.Decryptor.cs
new file mode 100644
index 0000000000..f43b1e9776
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaUtil.Decryptor.cs
@@ -0,0 +1,56 @@
+using CollapseLauncher.Helper.StreamUtility;
+using System;
+using System.IO;
+using System.Security.Cryptography;
+#pragma warning disable IDE0130
+
+#nullable enable
+namespace CollapseLauncher.Helper.DaftarPustaka;
+
+internal static partial class DaftarPustakaUtil
+{
+ public static class Decryptor
+ {
+ public static CryptoStream CreateAesStream(
+ Stream inputStream,
+ ServeV3FileContext context,
+ bool leaveOpen = false)
+ {
+ CipherMode cipherMode = inputStream.Read();
+ PaddingMode paddingMode = inputStream.Read();
+ byte keySize = inputStream.Read();
+ byte ivSize = inputStream.Read();
+
+ context.GetKeyAndIvFromLength(keySize,
+ ivSize,
+ out byte[] key,
+ out byte[] iv);
+
+ Aes aesInstance = Aes.Create();
+ aesInstance.Mode = cipherMode;
+ aesInstance.Padding = paddingMode;
+ aesInstance.Key = key;
+
+ if (cipherMode != CipherMode.ECB)
+ aesInstance.IV = iv;
+
+ return new CryptoStream(inputStream, aesInstance.CreateDecryptor(), CryptoStreamMode.Read, leaveOpen);
+ }
+
+ public static CryptoStream CreateRsaStream(
+ Stream inputStream,
+ ServeV3FileContext context,
+ bool leaveOpen = false)
+ {
+ throw new NotImplementedException();
+ }
+
+ public static CryptoStream CreateXorStream(
+ Stream inputStream,
+ ServeV3FileContext context,
+ bool leaveOpen = false)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
diff --git a/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaUtil.Reader.cs b/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaUtil.Reader.cs
new file mode 100644
index 0000000000..b18ec10fdd
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/DaftarPustaka/DaftarPustakaUtil.Reader.cs
@@ -0,0 +1,60 @@
+using CollapseLauncher.Helper.StreamUtility;
+using System;
+using System.IO;
+#pragma warning disable IDE0130
+
+#nullable enable
+namespace CollapseLauncher.Helper.DaftarPustaka;
+
+internal static partial class DaftarPustakaUtil
+{
+ public static class Reader
+ {
+ public static Stream CreateCryptoStream(
+ Stream inputStream,
+ ServeV3FileContext? context = null,
+ bool leaveOpen = false)
+ {
+ ulong signature = inputStream.Read();
+ ushort version = inputStream.Read();
+
+ if (signature != DaftarPustakaSettings.CollapseHeader)
+ throw new InvalidDataException("Stream does not contain Daftar Pustaka formatted file.");
+
+ if (version != DaftarPustakaSettings.CurrentVersion)
+ throw new InvalidDataException($"Daftar Pustaka version: {version} is not supported!");
+
+ DaftarPustakaCompression compressionType = inputStream.Read();
+ DaftarPustakaEncryption encryptionType = inputStream.Read();
+
+ if (context == null && encryptionType != DaftarPustakaEncryption.Unencrypted)
+ throw new ArgumentNullException(nameof(context), "Context cannot be null while encryption is set!");
+
+ inputStream = encryptionType switch
+ {
+ DaftarPustakaEncryption.Aes => Decryptor.CreateAesStream(inputStream, context!, leaveOpen),
+ DaftarPustakaEncryption.Rsa => Decryptor.CreateRsaStream(inputStream, context!, leaveOpen),
+ DaftarPustakaEncryption.Xor => Decryptor.CreateXorStream(inputStream, context!, leaveOpen),
+ DaftarPustakaEncryption.Unencrypted => inputStream,
+
+ _ => throw new NotSupportedException($"Encryption type: {encryptionType} is not implemented!")
+ };
+
+ inputStream = compressionType switch
+ {
+ DaftarPustakaCompression.Brotli => Decompressor.CreateBrotliStream(inputStream, leaveOpen),
+ DaftarPustakaCompression.Zstandard => Decompressor.CreateZStandardStream(inputStream, leaveOpen),
+ DaftarPustakaCompression.Deflate => Decompressor.CreateDeflateStream(inputStream, leaveOpen),
+ DaftarPustakaCompression.Zlib => Decompressor.CreateZlibStream(inputStream, leaveOpen),
+ DaftarPustakaCompression.Lzma => Decompressor.CreateLzmaStream(inputStream, leaveOpen),
+ DaftarPustakaCompression.Lzma2 => Decompressor.CreateLzmaStream(inputStream, leaveOpen),
+ DaftarPustakaCompression.BZip2 => Decompressor.CreateBZip2Stream(inputStream, leaveOpen),
+ DaftarPustakaCompression.Uncompressed => inputStream,
+
+ _ => throw new NotSupportedException($"Compression type: {compressionType} is not implemented!")
+ };
+
+ return inputStream;
+ }
+ }
+}
diff --git a/CollapseLauncher/Classes/Helper/DaftarPustaka/ServeV3FileContext.cs b/CollapseLauncher/Classes/Helper/DaftarPustaka/ServeV3FileContext.cs
new file mode 100644
index 0000000000..fcec6a95a4
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/DaftarPustaka/ServeV3FileContext.cs
@@ -0,0 +1,158 @@
+using Hi3Helper.EncTool;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.IO.Hashing;
+using System.Net.Http;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+#pragma warning disable IDE0130
+
+#nullable enable
+namespace CollapseLauncher.Helper.DaftarPustaka;
+
+[JsonSerializable(typeof(Dictionary))]
+[JsonSourceGenerationOptions(WriteIndented = true,
+ IndentCharacter = ' ',
+ IndentSize = 2,
+ AllowTrailingCommas = true)]
+internal partial class ServeV3FileContextContext : JsonSerializerContext;
+
+public class ServeV3FileContext
+{
+ public byte[] OriginFileHash { get; set; } = [];
+ public byte[] ContentFileHash { get; set; } = [];
+
+ public string? DataHashType { get; set; }
+ public string? FilenameHashType { get; set; }
+
+ public DateTimeOffset FileTime { get; set; }
+ public long Salt { get; set; }
+
+ public Stream? Stream { get; set; }
+
+ public Dictionary? Metadata { get; set; }
+
+ public void GetKeyAndIvFromLength(int keyLength, int ivLength, out byte[] key, out byte[] iv)
+ {
+ Unsafe.SkipInit(out key);
+ Unsafe.SkipInit(out iv);
+
+ if (FileTime == default) throw new InvalidOperationException("FileTime property cannot be empty!");
+ if (Salt == 0) throw new InvalidOperationException("Salt property cannot be empty!");
+
+ key = new byte[keyLength];
+ iv = new byte[ivLength];
+
+ DateTimeOffset fileTime = FileTime;
+ long dateTimeAsLong = MemoryMarshal.Read(MemoryMarshal.AsBytes(new ReadOnlySpan(ref fileTime)));
+
+ int saltLow = unchecked((int)Salt);
+ int saltHigh = unchecked((int)(Salt >> 32));
+
+ int dateTimeLow = unchecked((int)dateTimeAsLong);
+ int dateTimeHigh = unchecked((int)(dateTimeAsLong >> 32));
+
+ Random random = new(((saltLow | saltHigh) - (dateTimeLow | dateTimeHigh)) | (keyLength * ivLength));
+
+ if (key.Length != 0)
+ {
+ Array.Copy(ContentFileHash,
+ 0,
+ key,
+ 0,
+ Math.Min(ContentFileHash.Length, key.Length));
+
+ for (int i = 0; i < keyLength; i++)
+ key[i] |= (byte)random.Next();
+ }
+
+ if (iv.Length != 0)
+ {
+ Array.Copy(ContentFileHash,
+ 0,
+ iv,
+ 0,
+ Math.Min(ContentFileHash.Length, iv.Length));
+
+ for (int i = 0; i < ivLength; i++)
+ iv[i] |= (byte)random.Next();
+ }
+ }
+
+ public bool IsKeyStoreExist(string key) => Metadata?.ContainsKey(key) ?? false;
+
+ public string GetOriginalFileUrl()
+ {
+ const string dictKey = "origUrl";
+ if (!TryReadStringStoreAs(dictKey, out string? result))
+ {
+ throw new KeyNotFoundException("origUrl from pustaka's store is not exist. Please report this issue to our Discord Server!");
+ }
+
+ return string.IsNullOrEmpty(result)
+ ? throw new NullReferenceException("origUrl from pustaka's store is null or just an empty string. Please report this issue to our Discord Server!")
+ : result;
+ }
+
+ public bool TryReadStringStoreAs(string key, out string? result)
+ {
+ result = null;
+ if (!IsKeyStoreExist(key))
+ {
+ return false;
+ }
+
+ ReadOnlySpan dataSpan = Metadata?[key];
+ result = Encoding.UTF8.GetString(dataSpan);
+ return true;
+ }
+
+ public async Task GetOriginalFileHttpResponse(
+ HttpClient client,
+ HttpMethod? method = null,
+ CancellationToken token = default)
+ {
+ string originalUrl = GetOriginalFileUrl();
+ return await client.TryGetCachedStreamFrom(originalUrl, method, token);
+ }
+
+ public static string GetHashStringAuto(string source, ServeV3FileContext context)
+ => GetHashStringAuto(source.AsSpan(), context);
+
+ public static string GetHashStringAuto(ReadOnlySpan source, ServeV3FileContext context)
+ {
+ return context.FilenameHashType switch
+ {
+ nameof(XxHash3) => GetHashString(source),
+ nameof(XxHash32) => GetHashString(source),
+ nameof(XxHash64) => GetHashString(source),
+ nameof(XxHash128) => GetHashString(source),
+ nameof(Crc32) => GetHashString(source),
+ nameof(Crc64) => GetHashString(source),
+
+ _ => throw new NotSupportedException($"FilenameHashType: {context.FilenameHashType} is not supported")
+ };
+ }
+
+ public static string GetHashString(ReadOnlySpan source)
+ where T : NonCryptographicHashAlgorithm, new()
+ {
+ T hasher = new();
+
+ Span hashBuffer = stackalloc byte[hasher.HashLengthInBytes];
+ Span hashCharBuffer = stackalloc char[hasher.HashLengthInBytes * 2];
+
+ ReadOnlySpan sourceAsBytes = MemoryMarshal.AsBytes(source);
+ hasher.Append(sourceAsBytes);
+ hasher.GetHashAndReset(hashBuffer);
+
+ Convert.TryToHexStringLower(hashBuffer, hashCharBuffer, out _);
+
+ return new string(hashCharBuffer);
+ }
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs
index 69b5bb6e6e..e5f5e467fc 100644
--- a/CollapseLauncher/Classes/Helper/Database/DBHandler.cs
+++ b/CollapseLauncher/Classes/Helper/Database/DBHandler.cs
@@ -2,6 +2,7 @@
using Hi3Helper.SentryHelper;
using Libsql.Client;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
@@ -29,7 +30,7 @@ public static bool? IsEnabled
field = value;
DbConfig.DbEnabled = value ?? false;
- _isFirstInit = true; // Force first init
+ IsInitialized = false; // Force first init
if (!(value ?? false)) Dispose(); // Dispose instance if user disabled database function globally
}
}
@@ -45,11 +46,11 @@ public static string? Uri
}
set
{
- if (value != field) _isFirstInit = true; // Force first init if value changed
+ if (value != field) IsInitialized = false; // Force first init if value changed
field = value;
DbConfig.DbUrl = value;
- _isFirstInit = true;
+ IsInitialized = false;
}
}
@@ -65,11 +66,11 @@ public static string? Token
}
set
{
- if (value != field) _isFirstInit = true; // Force first init if value changed
+ if (value != field) IsInitialized = false; // Force first init if value changed
field = value;
DbConfig.DbToken = value;
- _isFirstInit = true;
+ IsInitialized = false;
}
}
@@ -93,7 +94,7 @@ public static string? UserId
}
set
{
- if (value != field) _isFirstInit = true; // Force first init if value changed
+ if (value != field) IsInitialized = false; // Force first init if value changed
field = value;
DbConfig.UserGuid = value;
@@ -101,17 +102,18 @@ public static string? UserId
if (string.IsNullOrWhiteSpace(value))
{
_userIdHash = null;
- _isFirstInit = true;
+ IsInitialized = false;
return;
}
var byteUidH = System.IO.Hashing.XxHash64.Hash(Encoding.ASCII.GetBytes(value));
_userIdHash = Convert.ToHexStringLower(byteUidH);
- _isFirstInit = true;
+ IsInitialized = false;
}
}
+
+ public static bool IsInitialized { get; private set; } = false;
- private static bool _isFirstInit = true;
#endregion
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
@@ -156,14 +158,18 @@ public static async Task Init(bool redirectThrow = false, bool bypassEnableFlag
opts.AuthToken = Token;
});
- if (_isFirstInit)
+ if (!IsInitialized)
{
LogWriteLine("[DbHandler::Init] Initializing database system...");
// Ensure table exist at first initialization
await
_database
.Execute($"CREATE TABLE IF NOT EXISTS \"uid-{_userIdHash}\" (Id INTEGER PRIMARY KEY AUTOINCREMENT, 'key' TEXT UNIQUE NOT NULL, 'value' TEXT)");
- _isFirstInit = false;
+
+ await
+ _database
+ .Execute($"CREATE TABLE IF NOT EXISTS \"uid-{_userIdHash}-blob\" (Id INTEGER PRIMARY KEY AUTOINCREMENT, 'key' TEXT UNIQUE NOT NULL, 'value' BLOB)");
+ IsInitialized = true;
}
else LogWriteLine("[DbHandler::Init] Reinitializing database system...");
}
@@ -218,7 +224,7 @@ private static void Dispose()
private const int MaxAttempts = 5;
- public static async Task QueryKey(string key, bool redirectThrow = false)
+ public static async Task QueryKey(string key, bool redirectThrow = false, bool isBlob = false)
{
if (!(IsEnabled ?? false)) return null;
#if DEBUG
@@ -229,7 +235,7 @@ private static void Dispose()
#endif
for (var i = 0; i < MaxAttempts; i++)
{
- var retVal = await QueryKeyInternal(key
+ var retVal = await QueryKeyInternal(key, isBlob
#if DEBUG
, sId
#endif
@@ -237,8 +243,16 @@ private static void Dispose()
if (retVal.result == 200)
{
#if DEBUG
- LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{retVal.returnedValue}",
- LogType.Debug, true);
+ if (isBlob)
+ {
+ LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tIS BLOB",
+ LogType.Debug, true);
+ }
+ else
+ {
+ LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{retVal.returnedValue}",
+ LogType.Debug, true);
+ }
#endif
return retVal.returnedValue;
}
@@ -278,24 +292,43 @@ private static void Dispose()
return null;
}
- public static async Task StoreKeyValue(string key, string value, bool redirectThrow = false)
+ public static async Task StoreKeyValue(string key, string value, bool redirectThrow = false,
+ bool isBlob = false, byte[]? blobValue = null)
{
if (!(IsEnabled ?? false)) return;
#if DEBUG
var t = Stopwatch.StartNew();
var r = new Random();
var sId = Math.Abs(r.Next(0, 1000).ToString().GetHashCode());
- LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tValue: {value}", LogType.Debug,
- true);
+ if (isBlob)
+ {
+ LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tIS BLOB", LogType.Debug,
+ true);
+ }
+ else
+ {
+ LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Invoked!\r\n\tKey: {key}\r\n\tValue: {value}", LogType.Debug,
+ true);
+ }
+
#endif
for (var i = 0; i < MaxAttempts; i++)
{
- var retVal = await StoreKeyValueInternal(key, value);
+ var retVal = await StoreKeyValueInternal(key, value, isBlob, blobValue);
if (retVal.result == 200)
{
#if DEBUG
- LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Saved value!\r\n\tKey: {key}\r\n\tValue: {value}",
- LogType.Debug, true);
+ if (isBlob)
+ {
+ LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Saved value!\r\n\tKey: {key}\r\n\tIS BLOB",
+ LogType.Debug, true);
+ }
+ else
+ {
+ LogWriteLine($"[DBHandler::StoreKeyValue][{sId}] Saved value!\r\n\tKey: {key}\r\n\tValue: {value}",
+ LogType.Debug, true);
+ }
+
#endif
return;
}
@@ -338,7 +371,7 @@ public static async Task StoreKeyValue(string key, string value, bool redirectTh
#region Private Methods
- private static async Task<(int result, string? returnedValue, Exception? exceptionValue)> QueryKeyInternal(string key
+ private static async Task<(int result, string? returnedValue, Exception? exceptionValue)> QueryKeyInternal(string key, bool isBlob
#if DEBUG
, int sId = 0
#endif
@@ -347,22 +380,53 @@ public static async Task StoreKeyValue(string key, string value, bool redirectTh
try
{
if (_database == null) await Init(true);
+ var tableName = "uid-" + _userIdHash + (isBlob ? "-blob" : "");
// Get table row for exact key
var rs =
await
_database!
- .Execute($"SELECT value FROM \"uid-{_userIdHash}\" WHERE key = ?", key);
+ .Execute($"SELECT value FROM \"{tableName}\" WHERE key = ?", key);
if (rs == null)
{
return (200, null, null);
}
+
+ string str = "";
- // freaking black magic to convert the column row to the value
- var str =
- string.Join("", rs.Rows.Select(row => string.Join("", row.Select(x => x.ToString()))));
+ if (isBlob)
+ {
+ var firstRow = rs.Rows.FirstOrDefault();
+ object? rcv = firstRow?.FirstOrDefault();
+
+ if (rcv is Blob { Value: IEnumerable byteEnumerable })
+ {
+ str = Convert.ToHexString(byteEnumerable.ToArray());
+ }
+ // ReSharper disable once ConvertTypeCheckPatternToNullCheck
+ else if (rcv is Blob { Value: byte[] directBytes })
+ {
+ str = Convert.ToHexString(directBytes);
+ }
+ }
+ else
+ {
+ // freaking black magic to convert the column row to the value
+ str =
+ string.Join("", rs.Rows.Select(row => string.Join("", row.Select(x => x.ToString()))));
+ }
+
#if DEBUG
- LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{str}", LogType.Debug,
- true);
+ if (isBlob)
+ {
+ LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tIS BLOB", LogType.Debug,
+ true);
+ }
+ else
+ {
+ LogWriteLine($"[DBHandler::QueryKey][{sId}] Got value!\r\n\tKey: {key}\r\n\tValue:\r\n{str}", LogType.Debug,
+ true);
+ }
+
#endif
return (200, str, null); // 200: OK, return value
}
@@ -386,16 +450,18 @@ public static async Task StoreKeyValue(string key, string value, bool redirectTh
}
}
- private static async Task<(int result, Exception? exceptionValue)> StoreKeyValueInternal(string key, string value)
+ private static async Task<(int result, Exception? exceptionValue)> StoreKeyValueInternal(string key, string value, bool isBlob = false, byte[]? blobValue = null)
{
try
{
if (_database == null) await Init(true);
+ var tableName = "uid-" + _userIdHash + (isBlob ? "-blob" : "");
+ object dbValue = isBlob && blobValue != null ? blobValue : value;
// Create key for storing value, if key already exist, just update the value (key column is set to UNIQUE)
- var command = $"INSERT INTO \"uid-{_userIdHash}\" (key, value) VALUES (?, ?) " +
+ var command = $"INSERT INTO \"{tableName}\" (key, value) VALUES (?, ?) " +
$"ON CONFLICT(key) DO UPDATE SET value = ?";
- var parameters = new object[] { key, value, value };
+ var parameters = new object[] { key, dbValue, dbValue };
await _database!.Execute(command, parameters);
return (200, null); // 200: OK
diff --git a/CollapseLauncher/Classes/Helper/FFmpegCodecInstaller.cs b/CollapseLauncher/Classes/Helper/FFmpegCodecInstaller.cs
index 52765a5a4e..2791dee277 100644
--- a/CollapseLauncher/Classes/Helper/FFmpegCodecInstaller.cs
+++ b/CollapseLauncher/Classes/Helper/FFmpegCodecInstaller.cs
@@ -1,5 +1,6 @@
using CollapseLauncher.Extension;
using CollapseLauncher.GameManagement.ImageBackground;
+using CollapseLauncher.Helper.InternalPInvoke;
using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.Interfaces;
using Hi3Helper.Data;
@@ -112,7 +113,7 @@ await extractDelegate(() => File.Open(packageFile,
extractFolder,
token);
- string innerFfmpegDir = ImageBackgroundManager.FindFFmpegInstallFolder(extractFolder, ImageBackgroundManager.Shared.GlobalFFmpegLibraryNames) ??
+ string innerFfmpegDir = FFmpegPInvoke.FindFFmpegInstallFolder(extractFolder, ImageBackgroundManager.Shared.GlobalFFmpegLibraryNames) ??
throw new FileNotFoundException("Library files are not found!");
foreach (string dllFile in Directory.EnumerateFiles(innerFfmpegDir, "*.dll", SearchOption.TopDirectoryOnly))
diff --git a/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs b/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs
index c839b16361..1c6abf9950 100644
--- a/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs
+++ b/CollapseLauncher/Classes/Helper/Image/ImageLoaderHelper.cs
@@ -1,4 +1,5 @@
-using CollapseLauncher.Helper.StreamUtility;
+using CollapseLauncher.GameManagement.ImageBackground;
+using CollapseLauncher.Helper.StreamUtility;
using CollapseLauncher.Plugins;
using CollapseLauncher.XAMLs.Theme.CustomControls;
using Hi3Helper;
@@ -35,21 +36,6 @@ namespace CollapseLauncher.Helper.Image
{
internal static class ImageLoaderHelper
{
- internal static readonly string SupportedImageFormats =
- string.Join(";", LayeredBackgroundImage.SupportedImageBitmapExtensions.Select(x => $"*{x}")) + ";" +
- string.Join(";", LayeredBackgroundImage.SupportedImageBitmapExternalCodecExtensions.Select(x => $"*{x}")) + ";" +
- string.Join(";", LayeredBackgroundImage.SupportedImageVectorExtensions.Select(x => $"*{x}"));
-
- internal static readonly string SupportedVideoFormats =
- string.Join(";", LayeredBackgroundImage.SupportedVideoExtensions.Select(x => $"*{x}"));
-
- internal static readonly Dictionary SupportedBackgroundFormats =
- new() {
- { "All supported formats", SupportedImageFormats + ';' + SupportedVideoFormats },
- { "Image formats", SupportedImageFormats },
- { "Video formats", SupportedVideoFormats }
- };
-
#region Waifu2X
public static Waifu2X Waifu2X;
private static Waifu2XStatus _cachedStatus = Waifu2XStatus.NotInitialized;
@@ -420,8 +406,12 @@ private static async Task GetFallbackStreamUrlInner(HttpClient? client,
public static async Task GetCachedSpritesAsync(HttpClient? httpClient, string? url, bool isSkipHashCheck, CancellationToken token)
{
- if (string.IsNullOrEmpty(url) ||
- ProcessingUrls.Contains(url))
+ if (string.IsNullOrEmpty(url))
+ {
+ return null;
+ }
+
+ if (ProcessingUrls.Contains(url))
{
Logger.LogWriteLine("Found duplicate download request, skipping...\r\n\t" +
$"URL : {url}", LogType.Warning, true);
@@ -432,7 +422,7 @@ private static async Task GetFallbackStreamUrlInner(HttpClient? client,
if (!Directory.Exists(AppGameImgCachedFolder))
Directory.CreateDirectory(AppGameImgCachedFolder);
- FileInfo fInfo = new FileInfo(cachePath);
+ FileInfo fInfo = new(cachePath);
if (await IsFileCompletelyDownloadedAsync(fInfo, !isSkipHashCheck))
{
return cachePath;
@@ -442,6 +432,7 @@ private static async Task GetFallbackStreamUrlInner(HttpClient? client,
{
return url;
}
+
return cachePath;
}
diff --git a/CollapseLauncher/Classes/Helper/Image/Waifu2X.cs b/CollapseLauncher/Classes/Helper/Image/Waifu2X.cs
index cb4cbf279b..0fe47486db 100644
--- a/CollapseLauncher/Classes/Helper/Image/Waifu2X.cs
+++ b/CollapseLauncher/Classes/Helper/Image/Waifu2X.cs
@@ -219,7 +219,7 @@ public static Waifu2XStatus VulkanTest()
Logger.LogWriteLine("D3DMappingLayers package detected. Fallback to CPU mode.", LogType.Warning, true);
return Waifu2XStatus.D3DMappingLayers;
}
- var status = Waifu2XPInvoke.waifu2x_self_test(0);
+ Waifu2XStatus status = Waifu2XPInvoke.waifu2x_self_test(0);
switch (status)
{
case Waifu2XStatus.CpuMode:
@@ -248,7 +248,7 @@ public static Waifu2XStatus VulkanTest()
return ReturnAsFailedDllInit(ex);
}
- Waifu2XStatus ReturnAsFailedDllInit(T ex)
+ static Waifu2XStatus ReturnAsFailedDllInit(T ex)
where T : Exception
{
Logger.LogWriteLine($"Cannot load Waifu2X as the library failed to load!\r\n{ex}", LogType.Error, true);
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/CollapsePInvoke.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/CollapsePInvoke.cs
index 228113679b..fcc5d6b478 100644
--- a/CollapseLauncher/Classes/Helper/InternalPInvoke/CollapsePInvoke.cs
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/CollapsePInvoke.cs
@@ -1,19 +1,34 @@
using Hi3Helper;
using Hi3Helper.Shared.Region;
using System;
+using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+// ReSharper disable StringLiteralTypo
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
namespace CollapseLauncher.Helper.InternalPInvoke;
internal static class CollapsePInvoke
{
- private const string LibraryExtension = ".dll";
+ internal const string LibraryExtension = ".dll";
+
+ public static Dictionary CustomResolvers = new(StringComparer.OrdinalIgnoreCase);
internal static nint DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
+ // Try to resolve the library from the Custom Resolvers first.
+ if (CustomResolvers.TryGetValue(libraryName, out DllImportResolver customResolver) &&
+ customResolver(libraryName, assembly, searchPath) is var customResolverResult &&
+ customResolverResult != nint.Zero)
+ {
+ return customResolverResult;
+ }
+
bool retryFirst = false;
string pathToLoad = libraryName;
LoadFirst:
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVCodecParameters.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVCodecParameters.cs
new file mode 100644
index 0000000000..451fca1ffb
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVCodecParameters.cs
@@ -0,0 +1,24 @@
+using System.Runtime.InteropServices;
+// ReSharper disable IdentifierTypo
+// ReSharper disable UnusedMember.Global
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+
+[StructLayout(LayoutKind.Sequential)]
+public struct AVCodecParameters
+{
+ public AVMediaType codec_type;
+ public AVCodecID codec_id;
+
+ public uint codec_tag;
+
+ public byte* extradata;
+ public int extradata_size;
+
+ public void* coded_side_data;
+ public int nb_coded_side_data;
+
+ public int format;
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVFormatContext.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVFormatContext.cs
new file mode 100644
index 0000000000..128c0f349b
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVFormatContext.cs
@@ -0,0 +1,22 @@
+using System.Runtime.InteropServices;
+// ReSharper disable IdentifierTypo
+// ReSharper disable UnusedMember.Global
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+
+[StructLayout(LayoutKind.Sequential)]
+public unsafe struct AVFormatContext
+{
+ public void* av_class;
+ public void* iformat;
+ public void* oformat;
+ public void* priv_data;
+ public void* pb;
+
+ public int ctx_flags;
+ public uint nb_streams;
+
+ public AVStream** streams;
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVMediaType.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVMediaType.cs
index 56516236df..a907e5a1c6 100644
--- a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVMediaType.cs
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVMediaType.cs
@@ -1,4 +1,9 @@
-namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+// ReSharper disable StringLiteralTypo
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+// ReSharper disable CommentTypo
+#pragma warning disable IDE0130
+namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
// https://ffmpeg.org/doxygen/7.0/group__lavu__misc.html#ga9a84bba4713dfced21a1a56163be1f48
public enum AVMediaType
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVPixFmtDescriptor.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVPixFmtDescriptor.cs
new file mode 100644
index 0000000000..81a7840efd
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVPixFmtDescriptor.cs
@@ -0,0 +1,19 @@
+// ReSharper disable StringLiteralTypo
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
+using System.Runtime.InteropServices;
+
+namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+
+[StructLayout(LayoutKind.Sequential)]
+public struct AVPixFmtDescriptor
+{
+ public byte* Name;
+
+ public byte NbComponents;
+ public byte Log2ChromaW;
+ public byte Log2ChromaH;
+
+ public ulong Flags;
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVStream.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVStream.cs
new file mode 100644
index 0000000000..b33c100f1a
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/AVStream.cs
@@ -0,0 +1,18 @@
+using System.Runtime.InteropServices;
+// ReSharper disable IdentifierTypo
+// ReSharper disable UnusedMember.Global
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+
+[StructLayout(LayoutKind.Sequential)]
+public struct AVStream
+{
+ public void* av_class;
+
+ public int index;
+ public int id;
+
+ public AVCodecParameters* codecpar;
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/Const.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/Const.cs
new file mode 100644
index 0000000000..aceb494b97
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/Const.cs
@@ -0,0 +1,17 @@
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
+namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+
+public static class Const
+{
+ public const string AVCodecPrefix = "avcodec";
+ public const string AVDevicePrefix = "avdevice";
+ public const string AVFilterPrefix = "avfilter";
+ public const string AVFormatPrefix = "avformat";
+ public const string AVUtilPrefix = "avutil";
+ public const string SWResamplePrefix = "swresample";
+ public const string SWScalePrefix = "swscale";
+ public const string PostprocPrefix = "postproc";
+}
+
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/FFmpegLibraryNames.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/FFmpegLibraryNames.cs
new file mode 100644
index 0000000000..99ada37807
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/FFmpegLibraryNames.cs
@@ -0,0 +1,35 @@
+// ReSharper disable IdentifierTypo
+#pragma warning disable IDE0130
+namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+
+public struct FFmpegLibraryNames
+{
+ internal FFmpegLibraryNames(
+ int codecNum,
+ int deviceNum,
+ int filterNum,
+ int formatNum,
+ int utilNum,
+ int resampleNum,
+ int scaleNum,
+ int postprocNum)
+ {
+ Codec = Const.AVCodecPrefix + '-' + codecNum + CollapsePInvoke.LibraryExtension;
+ Device = Const.AVDevicePrefix + '-' + deviceNum + CollapsePInvoke.LibraryExtension;
+ Filter = Const.AVFilterPrefix + '-' + filterNum + CollapsePInvoke.LibraryExtension;
+ Format = Const.AVFormatPrefix + '-' + formatNum + CollapsePInvoke.LibraryExtension;
+ Util = Const.AVUtilPrefix + '-' + utilNum + CollapsePInvoke.LibraryExtension;
+ Resample = Const.SWResamplePrefix + '-' + resampleNum + CollapsePInvoke.LibraryExtension;
+ Scale = Const.SWScalePrefix + '-' + scaleNum + CollapsePInvoke.LibraryExtension;
+ PostProc = Const.PostprocPrefix + '-' + postprocNum + CollapsePInvoke.LibraryExtension;
+ }
+
+ public string Codec;
+ public string Device;
+ public string Filter;
+ public string Format;
+ public string Util;
+ public string Resample;
+ public string Scale;
+ public string PostProc;
+}
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/MediaSupport.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/MediaSupport.cs
new file mode 100644
index 0000000000..2d3efd50ec
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/MediaSupport.cs
@@ -0,0 +1,31 @@
+// ReSharper disable StringLiteralTypo
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
+
+#nullable enable
+
+namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+internal readonly record struct StreamSupport(
+ bool Exists,
+ bool DecoderAvailable,
+ int StreamIndex);
+
+internal readonly record struct VideoPixelFormatInfo(
+ AVPixelFormat Format,
+ string? Name,
+ PixelColorModel ColorModel,
+ bool IsGrayscale,
+ int ComponentCount,
+ int Log2ChromaWidth,
+ int Log2ChromaHeight);
+
+internal readonly record struct MediaSupport(
+ StreamSupport Video,
+ VideoPixelFormatInfo VideoPixelFormatInfo,
+ StreamSupport Audio)
+{
+ public bool IsSupported =>
+ (!Video.Exists || Video.DecoderAvailable) &&
+ (!Audio.Exists || Audio.DecoderAvailable);
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/PixelColorModel.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/PixelColorModel.cs
new file mode 100644
index 0000000000..aa275e8484
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpeg/PixelColorModel.cs
@@ -0,0 +1,25 @@
+// ReSharper disable StringLiteralTypo
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+
+using System;
+
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+
+[Flags]
+public enum PixelColorModel : ulong
+{
+ Unknown,
+ BigEndian = 1UL << 0,
+ Pal = 1UL << 1,
+ Bitstream = 1UL << 2,
+ Hardware = 1UL << 3,
+ Planar = 1UL << 4,
+ Rgb = 1UL << 5,
+ Alpha = 1UL << 7,
+ Bayer = 1UL << 8,
+ Float = 1UL << 9,
+ Xyz = 1UL << 10,
+}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.ExternFormat.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.ExternFormat.cs
new file mode 100644
index 0000000000..d50b01b0f6
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.ExternFormat.cs
@@ -0,0 +1,44 @@
+using CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+using System.Runtime.InteropServices;
+// ReSharper disable StringLiteralTypo
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.Helper.InternalPInvoke;
+
+public static unsafe partial class FFmpegPInvoke
+{
+ public static unsafe partial class ExternFormat
+ {
+ [LibraryImport(Const.AVFormatPrefix,
+ EntryPoint = "avformat_open_input",
+ StringMarshalling = StringMarshalling.Utf8)]
+ public static partial int avformat_open_input(
+ AVFormatContext** context,
+ string url,
+ void* fmt,
+ void* options);
+
+ [LibraryImport(Const.AVFormatPrefix,
+ EntryPoint = "avformat_find_stream_info")]
+ public static partial int avformat_find_stream_info(
+ AVFormatContext* context,
+ void* options);
+
+ [LibraryImport(Const.AVFormatPrefix,
+ EntryPoint = "av_find_best_stream")]
+ public static partial int av_find_best_stream(
+ AVFormatContext* context,
+ AVMediaType type,
+ int wantedStreamNumber,
+ int relatedStream,
+ ref nint decoder,
+ int flags);
+
+ [LibraryImport(Const.AVFormatPrefix,
+ EntryPoint = "avformat_close_input")]
+ public static partial void avformat_close_input(
+ AVFormatContext** context);
+ }
+}
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.ExternUtil.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.ExternUtil.cs
new file mode 100644
index 0000000000..2384ae0fb6
--- /dev/null
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.ExternUtil.cs
@@ -0,0 +1,19 @@
+using CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+using System.Runtime.InteropServices;
+// ReSharper disable StringLiteralTypo
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
+
+namespace CollapseLauncher.Helper.InternalPInvoke;
+
+public static unsafe partial class FFmpegPInvoke
+{
+ public static unsafe partial class ExternUtil
+ {
+ [LibraryImport(Const.AVUtilPrefix,
+ EntryPoint = "av_pix_fmt_desc_get")]
+ public static partial AVPixFmtDescriptor* av_pix_fmt_desc_get(
+ AVPixelFormat pixelFormat);
+ }
+}
diff --git a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.cs b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.cs
index b24012c92a..5c9ab43dda 100644
--- a/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.cs
+++ b/CollapseLauncher/Classes/Helper/InternalPInvoke/FFmpegPInvoke.cs
@@ -1,50 +1,387 @@
using CollapseLauncher.Helper.InternalPInvoke.FFmpeg;
+using Hi3Helper.Shared.Region;
using System;
+using System.Collections;
using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+// ReSharper disable StringLiteralTypo
+// ReSharper disable IdentifierTypo
+// ReSharper disable InconsistentNaming
+#pragma warning disable IDE0130
#nullable enable
-namespace CollapseLauncher.Helper.FFmpegPInvoke;
+namespace CollapseLauncher.Helper.InternalPInvoke;
public static unsafe partial class FFmpegPInvoke
{
+ static FFmpegPInvoke()
+ {
+ CollapsePInvoke.CustomResolvers.Add(Const.AVCodecPrefix, FFmpegCustomDllImportResolver);
+ CollapsePInvoke.CustomResolvers.Add(Const.AVDevicePrefix, FFmpegCustomDllImportResolver);
+ CollapsePInvoke.CustomResolvers.Add(Const.AVFilterPrefix, FFmpegCustomDllImportResolver);
+ CollapsePInvoke.CustomResolvers.Add(Const.AVFormatPrefix, FFmpegCustomDllImportResolver);
+ CollapsePInvoke.CustomResolvers.Add(Const.AVUtilPrefix, FFmpegCustomDllImportResolver);
+ CollapsePInvoke.CustomResolvers.Add(Const.SWResamplePrefix, FFmpegCustomDllImportResolver);
+ CollapsePInvoke.CustomResolvers.Add(Const.SWScalePrefix, FFmpegCustomDllImportResolver);
+ CollapsePInvoke.CustomResolvers.Add(Const.PostprocPrefix, FFmpegCustomDllImportResolver);
+ }
+
+ private const int GlobalFFmpegDefaultVersionKey = 80;
+ private const string GlobalFFmpegCustomPathConfigKey = "GlobalFFmpegCustomPath";
+ private const string GlobalFFmpegVersionToUseConfigKey = "GlobalFFmpegVersionToUse";
+
public static Dictionary FFmpegVersionLibNames = new()
{
- { 70, new FFmpegLibraryNames("avcodec-61.dll", "avdevice-61.dll", "avfilter-10.dll", "avformat-61.dll", "avutil-59.dll", "swresample-5.dll", "swscale-8.dll", "postproc-58.dll") },
- { 80, new FFmpegLibraryNames("avcodec-62.dll", "avdevice-62.dll", "avfilter-11.dll", "avformat-62.dll", "avutil-60.dll", "swresample-6.dll", "swscale-9.dll", "postproc-59.dll") }
+ { 70, new FFmpegLibraryNames(61, 61, 10, 61, 59, 5, 8, 58) },
+ { 80, new FFmpegLibraryNames(62, 62, 11, 62, 60, 6, 9, 59) }
};
- [LibraryImport("avcodec")]
- public static partial AVCodec* av_codec_iterate(ref nint opaque);
+ public static int FFmpegVersionToUse
+ {
+ get
+ {
+ int version = LauncherConfig.GetAppConfigValue(GlobalFFmpegVersionToUseConfigKey);
+ return FFmpegVersionLibNames.ContainsKey(version) ?
+ version :
+ GlobalFFmpegDefaultVersionKey;
+ }
+ set
+ {
+ if (!FFmpegVersionLibNames.ContainsKey(value))
+ {
+ return;
+ }
+
+ LauncherConfig.SetAndSaveConfigValue(GlobalFFmpegVersionToUseConfigKey, value);
+ }
+ }
+
+ public static FFmpegLibraryNames FFmpegLibraryNamings
+ {
+ get
+ {
+ if (FFmpegVersionLibNames.TryGetValue(FFmpegVersionToUse, out FFmpegLibraryNames names))
+ {
+ return names;
+ }
+
+ return FFmpegVersionLibNames.TryGetValue(GlobalFFmpegDefaultVersionKey, out names) ? names :
+ FFmpegVersionLibNames.Values.FirstOrDefault();
+ }
+ }
- public static IEnumerable EnumerateSupportedCodecs()
- => EnumerateSupportedCodecs([]);
+ public static string? CustomFFmpegPath
+ {
+ get => LauncherConfig.GetAppConfigValue(GlobalFFmpegCustomPathConfigKey);
+ set => LauncherConfig.SetAndSaveConfigValue(GlobalFFmpegCustomPathConfigKey, value);
+ }
- public static IEnumerable EnumerateSupportedCodecs(params AVMediaType[] codecTypes)
+ private static nint FFmpegCustomDllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
- nint opaque = nint.Zero;
- scoped ref AVCodec codecPtr = ref Unsafe.NullRef();
+ FFmpegLibraryNames namings = FFmpegLibraryNamings;
+ string libraryFilename = libraryName switch
+ {
+ Const.AVCodecPrefix => namings.Codec,
+ Const.AVDevicePrefix => namings.Device,
+ Const.AVFilterPrefix => namings.Filter,
+ Const.AVFormatPrefix => namings.Format,
+ Const.AVUtilPrefix => namings.Util,
+ Const.SWResamplePrefix => namings.Resample,
+ Const.SWScalePrefix => namings.Scale,
+ Const.PostprocPrefix => namings.PostProc,
+ _ => throw new InvalidOperationException($"{libraryName} is not included in FFmpegPInvoke list")
+ };
- Iterate:
- codecPtr = ref IterateCodec(ref opaque);
- if (Unsafe.IsNullRef(in codecPtr))
+ string? customPath = CustomFFmpegPath;
+
+ // 1. Try to load from custom path first
+ if (!string.IsNullOrEmpty(customPath) &&
+ IsFFmpegAvailable(customPath, namings, out _) &&
+ TryLoad(Path.Combine(customPath, libraryFilename), out nint handle))
+ {
+ return handle;
+ }
+
+ // 2. Try to find on the current linked path
+ if (IsFFmpegAvailable(LauncherConfig.AppExecutableDir, namings, out _) &&
+ TryLoad(Path.Combine(LauncherConfig.AppExecutableDir, libraryFilename), out handle))
{
- yield break;
+ return handle;
}
- if (codecTypes.Length != 0 &&
- !codecTypes.Contains(codecPtr.Type))
+ // 3. Try to find from environment variable
+ if (TryFindFFmpegInstallFromEnvVar(namings, out string? envVarPath, out _) &&
+ TryLoad(Path.Combine(envVarPath, libraryFilename), out handle))
{
- goto Iterate;
+ return handle;
}
- yield return codecPtr;
- goto Iterate;
+ // Otherwise, return null
+ return nint.Zero;
+
+ static bool TryLoad(string fullPath, out nint handle)
+ {
+ if (FileUtility.IsFileExistOrSymbolicLinkResolved(fullPath, out string? resolvedPath, out _))
+ {
+ fullPath = resolvedPath;
+ }
+
+ return NativeLibrary.TryLoad(fullPath, out handle);
+ }
}
- private static ref AVCodec IterateCodec(ref nint opaque)
- => ref Unsafe.AsRef(av_codec_iterate(ref opaque));
+ internal static bool IsFFmpegAvailable(
+ string? checkOnDirectory,
+ FFmpegLibraryNames libraries,
+ [NotNullWhen(false)]
+ out Exception? exception)
+ {
+ if (string.IsNullOrEmpty(checkOnDirectory))
+ {
+ exception = new NullReferenceException($"Argument: {nameof(checkOnDirectory)} is null!");
+ return false;
+ }
+
+ checkOnDirectory = FileUtility.GetFullyQualifiedPath(checkOnDirectory);
+
+ string dllPathAvcodec = Path.Combine(checkOnDirectory, libraries.Codec);
+ string dllPathAvdevice = Path.Combine(checkOnDirectory, libraries.Device);
+ string dllPathAvfilter = Path.Combine(checkOnDirectory, libraries.Filter);
+ string dllPathAvformat = Path.Combine(checkOnDirectory, libraries.Format);
+ string dllPathAvutil = Path.Combine(checkOnDirectory, libraries.Util);
+ string dllPathSwresample = Path.Combine(checkOnDirectory, libraries.Resample);
+ string dllPathSwscale = Path.Combine(checkOnDirectory, libraries.Scale);
+
+ return FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvcodec, out _, out exception) &&
+ FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvdevice, out _, out exception) &&
+ FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvfilter, out _, out exception) &&
+ FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvformat, out _, out exception) &&
+ FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathAvutil, out _, out exception) &&
+ FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathSwresample, out _, out exception) &&
+ FileUtility.IsFileExistOrSymbolicLinkResolved(dllPathSwscale, out _, out exception);
+ }
+
+ internal static bool TryFindFFmpegInstallFromEnvVar(
+ FFmpegLibraryNames libraries,
+ [NotNullWhen(true)] out string? path,
+ out Exception? exception)
+ {
+ return FindIn(EnvironmentVariableTarget.User, out path, out exception) ||
+ FindIn(EnvironmentVariableTarget.Machine, out path, out exception);
+
+ bool FindIn(EnvironmentVariableTarget target, [NotNullWhen(true)] out string? innerPath, out Exception? exception)
+ {
+ const string separators = ";,";
+ Unsafe.SkipInit(out innerPath);
+ Unsafe.SkipInit(out exception);
- public record struct FFmpegLibraryNames(string Codec, string Device, string Filter, string Format, string Util, string Resample, string Scale, string PostProc);
+ foreach (object? varValue in Environment.GetEnvironmentVariables(target))
+ {
+ if (varValue is not DictionaryEntry { Value: string varValueStr })
+ {
+ continue;
+ }
+
+ ReadOnlySpan envVar = varValueStr;
+ foreach (Range envVarRange in envVar.SplitAny(separators))
+ {
+ ReadOnlySpan envVarPath = envVar[envVarRange].Trim(" '\"");
+ if (envVarPath.IsEmpty)
+ {
+ continue;
+ }
+
+ string thisPath = envVarPath.ToString();
+
+ if (!Path.IsPathFullyQualified(thisPath) ||
+ !IsFFmpegAvailable(thisPath, libraries, out exception)) continue;
+
+ innerPath = thisPath;
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ internal static string? FindFFmpegInstallFolder(string checkOnDirectory, FFmpegLibraryNames libraries)
+ {
+ try
+ {
+ if (IsFFmpegAvailable(checkOnDirectory, libraries, out _))
+ {
+ return checkOnDirectory;
+ }
+
+ foreach (string dirPath in FileUtility.EnumerateDirectoryRecursive(checkOnDirectory))
+ {
+ if (IsFFmpegAvailable(dirPath, libraries, out _))
+ {
+ return dirPath;
+ }
+ }
+ }
+ catch
+ {
+ // ignored
+ }
+
+ return null;
+ }
+
+ internal static unsafe MediaSupport GetMediaSupport(string filePath)
+ {
+ AVFormatContext* formatContext = null;
+ int result = ExternFormat.avformat_open_input(&formatContext,
+ filePath,
+ null,
+ null);
+
+ if (result < 0)
+ {
+ return default;
+ }
+
+ try
+ {
+ result = ExternFormat.avformat_find_stream_info(formatContext, null);
+ if (result < 0)
+ {
+ return default;
+ }
+
+ StreamSupport video = CheckAVFormatStream(formatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, out int videoStreamIndex);
+ StreamSupport audio = CheckAVFormatStream(formatContext, AVMediaType.AVMEDIA_TYPE_AUDIO, out _);
+
+ VideoPixelFormatInfo videoPixelFormat = !video.DecoderAvailable
+ ? default
+ : GetAVFormatVideoPixelFormat(formatContext, videoStreamIndex);
+
+ return new MediaSupport(video, videoPixelFormat, audio);
+ }
+ finally
+ {
+ ExternFormat.avformat_close_input(&formatContext);
+ }
+ }
+
+ private static unsafe VideoPixelFormatInfo GetAVFormatVideoPixelFormat(AVFormatContext* context, int streamIndex)
+ {
+ AVStream* stream = context->streams[streamIndex];
+ AVCodecParameters* parameters = stream->codecpar;
+ AVPixelFormat pixelFormat = (AVPixelFormat)parameters->format;
+
+ if (pixelFormat == AVPixelFormat.AV_PIX_FMT_NONE)
+ {
+ return new VideoPixelFormatInfo(pixelFormat,
+ null,
+ PixelColorModel.Unknown,
+ false,
+ 0,
+ 0,
+ 0);
+ }
+
+ AVPixFmtDescriptor* descriptor = ExternUtil.av_pix_fmt_desc_get(pixelFormat);
+ string? name = ToString(descriptor->Name);
+
+ PixelColorModel model = GetPixelColorModel(descriptor, out bool isGrayscale);
+ return new VideoPixelFormatInfo(pixelFormat,
+ name,
+ model,
+ isGrayscale,
+ descriptor->NbComponents,
+ descriptor->Log2ChromaW,
+ descriptor->Log2ChromaH);
+
+ static string? ToString(byte* ptr) => ptr == null ? null : Marshal.PtrToStringUTF8((nint)ptr);
+ }
+
+ private static StreamSupport CheckAVFormatStream(
+ AVFormatContext* context,
+ AVMediaType type,
+ out int streamIndex)
+ {
+ streamIndex = ExternFormat.av_find_best_stream(context,
+ type,
+ -1,
+ -1,
+ ref Unsafe.NullRef(),
+ 0);
+
+ if (streamIndex < 0)
+ {
+ return new StreamSupport(Exists: false,
+ DecoderAvailable: false,
+ StreamIndex: -1);
+ }
+
+ nint decoder = nint.Zero;
+ int result = ExternFormat.av_find_best_stream(context,
+ type,
+ streamIndex,
+ -1,
+ ref decoder,
+ 0);
+
+ return new StreamSupport(Exists: true,
+ DecoderAvailable: result >= 0 && decoder != nint.Zero,
+ StreamIndex: streamIndex);
+ }
+
+ private static unsafe PixelColorModel GetPixelColorModel(AVPixFmtDescriptor* desc, out bool isGrayscale)
+ {
+ const ulong AV_PIX_FMT_FLAG_BE = 1UL << 0;
+ const ulong AV_PIX_FMT_FLAG_PAL = 1UL << 1;
+ const ulong AV_PIX_FMT_FLAG_BITSTREAM = 1UL << 2;
+ const ulong AV_PIX_FMT_FLAG_HWACCEL = 1UL << 3;
+ const ulong AV_PIX_FMT_FLAG_PLANAR = 1UL << 4;
+ const ulong AV_PIX_FMT_FLAG_RGB = 1UL << 5;
+ const ulong AV_PIX_FMT_FLAG_ALPHA = 1UL << 7;
+ const ulong AV_PIX_FMT_FLAG_BAYER = 1UL << 8;
+ const ulong AV_PIX_FMT_FLAG_FLOAT = 1UL << 9;
+ const ulong AV_PIX_FMT_FLAG_XYZ = 1UL << 10;
+
+ ulong flags = desc->Flags;
+ PixelColorModel pixelFormat = 0;
+
+ if ((flags & AV_PIX_FMT_FLAG_BE) != 0)
+ pixelFormat |= PixelColorModel.BigEndian;
+
+ if ((flags & AV_PIX_FMT_FLAG_PAL) != 0)
+ pixelFormat |= PixelColorModel.Pal;
+
+ if ((flags & AV_PIX_FMT_FLAG_BITSTREAM) != 0)
+ pixelFormat |= PixelColorModel.Bitstream;
+
+ if ((flags & AV_PIX_FMT_FLAG_HWACCEL) != 0)
+ pixelFormat |= PixelColorModel.Hardware;
+
+ if ((flags & AV_PIX_FMT_FLAG_PLANAR) != 0)
+ pixelFormat |= PixelColorModel.Planar;
+
+ if ((flags & AV_PIX_FMT_FLAG_RGB) != 0)
+ pixelFormat |= PixelColorModel.Rgb;
+
+ if ((flags & AV_PIX_FMT_FLAG_ALPHA) != 0)
+ pixelFormat |= PixelColorModel.Rgb;
+
+ if ((flags & AV_PIX_FMT_FLAG_BAYER) != 0)
+ pixelFormat |= PixelColorModel.Bayer;
+
+ if ((flags & AV_PIX_FMT_FLAG_FLOAT) != 0)
+ pixelFormat |= PixelColorModel.Float;
+
+ if ((flags & AV_PIX_FMT_FLAG_XYZ) != 0)
+ pixelFormat |= PixelColorModel.Xyz;
+
+ isGrayscale = desc->NbComponents <= 2;
+ return pixelFormat;
+ }
}
diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypApiLoader.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypApiLoader.cs
index bd41ef14e7..fddb575758 100644
--- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypApiLoader.cs
+++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypApiLoader.cs
@@ -358,14 +358,14 @@ private Task LoadLauncherGameInfo(ActionOnTimeOutRetry? onTimeoutRoutine,
bool isUseMultiLang = PresetConfig?.LauncherSpriteURLMultiLang ?? false;
- string localeCode = (isUseMultiLang ? Locale.Current.Lang?.LanguageID?.ToLower() : PresetConfig?.LauncherSpriteURLMultiLangFallback) ?? "";
+ string localeCode = (isUseMultiLang ? Locale.Current.Lang?.LanguageID?.ToLowerInvariant() : PresetConfig?.LauncherSpriteURLMultiLangFallback) ?? "";
string launcherGameInfoUrl = string.Format(PresetConfig?.LauncherGameInfoDisplayURL!, localeCode);
- ActionTimeoutTaskAwaitableCallback hypLauncherGameInfoCallback =
+ ActionTimeoutTaskAwaitableCallback hypLauncherGameInfoCallback =
innerToken =>
ApiResourceHttpClient
.GetFromCachedJsonAsync(launcherGameInfoUrl,
- HypApiJsonContext.Default.HypLauncherSophonBranchesApi,
+ HypApiJsonContext.Default.HypLauncherGetGameApi,
null,
innerToken)
.ConfigureAwait(false);
@@ -375,7 +375,16 @@ private Task LoadLauncherGameInfo(ActionOnTimeOutRetry? onTimeoutRoutine,
ExecutionTimeoutStep,
ExecutionTimeoutAttempt,
onTimeoutRoutine,
- result => LauncherGameSophonBranches = result,
+ result =>
+ {
+ LauncherGetGame = result;
+ if (result?.Data?.TryFindByBizOrId(PresetConfig?.LauncherBizName,
+ PresetConfig?.LauncherGameId,
+ out HypGameInfoData? gameInfoData) ?? false)
+ {
+ LauncherGameInfoField = gameInfoData;
+ }
+ },
token);
}
#endregion
diff --git a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypLauncherGameResourcePackageApi.cs b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypLauncherGameResourcePackageApi.cs
index 049bba709a..c4f8c505de 100644
--- a/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypLauncherGameResourcePackageApi.cs
+++ b/CollapseLauncher/Classes/Helper/LauncherApiLoader/HoYoPlay/HypLauncherGameResourcePackageApi.cs
@@ -69,10 +69,7 @@ public class HypPackageData
public byte[]? PackageMD5Hash { get; init; }
[JsonIgnore]
- public string? PackageMD5HashString
- {
- get => field ??= HexTool.BytesToHexUnsafe(PackageMD5Hash);
- }
+ public string? PackageMD5HashString => field ??= HexTool.BytesToHexUnsafe(PackageMD5Hash);
[JsonPropertyName("size")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
diff --git a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs
index bd519bfc2c..ba50fd1f71 100644
--- a/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs
+++ b/CollapseLauncher/Classes/Helper/Metadata/DataCooker.cs
@@ -4,6 +4,7 @@
using System.Buffers.Text;
using System.IO;
using System.IO.Compression;
+using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
@@ -12,7 +13,11 @@
// ReSharper disable IdentifierTypo
// ReSharper disable UnusedMember.Global
// ReSharper disable StringLiteralTypo
+#if NET11_0_OR_GREATER
+using ZstdDecompressStream = System.IO.Compression.ZstandardStream;
+#else
using ZstdDecompressStream = ZstdNet.DecompressionStream;
+#endif
#pragma warning disable IDE0130
#nullable enable
@@ -179,10 +184,10 @@ internal static void ServeV3Data(ReadOnlySpan data,
throw new FormatException($"Decompression format is not supported! ({compressionType})");
}
- #if DEBUG
+#if DEBUG
Logger.LogWriteLine($"[DataCooker::ServeV3Data()] Loaded ServeV3 data [IsPooled: {isDecryptPoolUsed}][TCompress: {compressionType} | IsEncrypt: {isUseEncryption}][CompSize: {compressedSize} | UncompSize: {decompressedSize}]",
LogType.Debug, true);
- #endif
+#endif
}
finally
{
@@ -214,28 +219,31 @@ private static int DecompressDataFromBrotli(Span outData, int compressedSi
private static unsafe int DecompressDataFromZstd(Span outData, int decompressedSize, ReadOnlySpan dataRawBuffer)
{
- fixed (byte* inputBuffer = &dataRawBuffer[0])
- fixed (byte* outputBuffer = &outData[0])
- {
- int decompressedWritten = 0;
+ byte* inputBuffer = (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(dataRawBuffer));
+ byte* outputBuffer = (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(outData));
+ int decompressedWritten = 0;
- byte[] buffer = new byte[4 << 10];
+ Span buffer = stackalloc byte[1 << 10];
- using UnmanagedMemoryStream inputStream = new(inputBuffer, dataRawBuffer.Length);
- using UnmanagedMemoryStream outputStream = new(outputBuffer, outData.Length);
- using ZstdDecompressStream decompStream = new(inputStream);
+ using UnmanagedMemoryStream inputStream = new(inputBuffer, dataRawBuffer.Length);
+ using UnmanagedMemoryStream outputStream = new(outputBuffer, outData.Length);
- int read;
- while ((read = decompStream.Read(buffer)) > 0)
- {
- outputStream.Write(buffer, 0, read);
- decompressedWritten += read;
- }
+#if NET11_0_OR_GREATER
+ using ZstdDecompressStream decompStream = new(inputStream, CompressionMode.Decompress);
+#else
+ using ZstdDecompressStream decompStream = new(inputStream);
+#endif
- return decompressedSize != decompressedWritten
- ? throw new DataMisalignedException("Decompressed data is misaligned!")
- : decompressedWritten;
- }
+ int read;
+ while ((read = decompStream.Read(buffer)) > 0)
+ {
+ outputStream.Write(buffer[..read]);
+ decompressedWritten += read;
+ }
+
+ return decompressedSize != decompressedWritten
+ ? throw new DataMisalignedException("Decompressed data is misaligned!")
+ : decompressedWritten;
}
}
}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/Helper/PatternMatcher.cs b/CollapseLauncher/Classes/Helper/PatternMatcher.cs
index cad29ade89..f653f6784e 100644
--- a/CollapseLauncher/Classes/Helper/PatternMatcher.cs
+++ b/CollapseLauncher/Classes/Helper/PatternMatcher.cs
@@ -8,7 +8,7 @@
namespace CollapseLauncher.Helper
{
- public static class PatternMatcher
+ public static partial class PatternMatcher
{
///
/// Determines whether the specified input string matches the given pattern.
@@ -132,5 +132,10 @@ public static string MergeRegexPattern(params ReadOnlySpan regexPatterns
return builder.ToString();
}
+
+ [GeneratedRegex(@"\.[0-9][0-9][0-9]$", RegexOptions.NonBacktracking)]
+ public static partial Regex MatchChunkFilePath();
+
+ public static bool IsChunkedFilePath(this string filePath) => MatchChunkFilePath().IsMatch(filePath);
}
}
\ No newline at end of file
diff --git a/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs b/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs
index 90df5161d7..33df15f4ba 100644
--- a/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs
+++ b/CollapseLauncher/Classes/Helper/StreamUtility/StreamExtension.cs
@@ -3,9 +3,12 @@
using Hi3Helper.SentryHelper;
using Hi3Helper.Win32.ManagedTools;
using System;
+using System.Buffers;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@@ -151,6 +154,9 @@ internal static FileInfo EnsureCreationOfDirectory(this FileInfo filePath)
}
}
+ internal static bool TryDeleteFile(string filePath, bool throwIfFailed = false)
+ => new FileInfo(filePath).TryDeleteFile(throwIfFailed);
+
internal static bool TryDeleteFile(this FileInfo filePath, bool throwIfFailed = false)
{
try
@@ -389,5 +395,123 @@ public static bool IsPathLocal(this ReadOnlySpan path)
path[1] == ':' &&
path[2] is '\\' or '/';
}
+
+ extension(Stream stream)
+ {
+ [SkipLocalsInit]
+ internal void Write(T source)
+ where T : unmanaged
+ {
+ ReadOnlySpan asSpan = MemoryMarshal.AsBytes(new ReadOnlySpan(in source));
+ stream.Write(asSpan);
+ }
+
+ [SkipLocalsInit]
+ internal unsafe void WriteArray(ReadOnlySpan source)
+ where T : unmanaged
+ {
+ ReadOnlySpan dataAsBytes = MemoryMarshal.AsBytes(source);
+ ushort sizeOfElement = (ushort)sizeof(T);
+
+ stream.Write(sizeOfElement);
+ stream.Write(dataAsBytes.Length);
+ stream.Write(dataAsBytes);
+ }
+
+ [SkipLocalsInit]
+ internal unsafe T Read()
+ where T : unmanaged
+ {
+ T result = default;
+ Span buffer = MemoryMarshal.AsBytes(new Span(ref result));
+
+ int read = stream.Read(buffer);
+ return read != sizeof(T)
+ ? throw new EndOfStreamException("Stream has been reaching end-of-data")
+ : result;
+ }
+
+ [SkipLocalsInit]
+ internal unsafe T[] ReadArray()
+ where T : unmanaged
+ {
+ int sizeOfElement = stream.Read();
+ int lengthInBytes = stream.Read();
+
+ return sizeOfElement != sizeof(T)
+ ? throw new InvalidCastException($"Data type size unmatched! Expecting: {sizeOfElement} bytes, but requesting ({typeof(T).Name}): {sizeof(T)} bytes instead!")
+ : stream.ReadArray(lengthInBytes / sizeOfElement);
+ }
+
+ internal unsafe T[] ReadArray