diff --git a/src/Starward.Language/Lang.Designer.cs b/src/Starward.Language/Lang.Designer.cs
index c14e2c908..f23b75100 100644
--- a/src/Starward.Language/Lang.Designer.cs
+++ b/src/Starward.Language/Lang.Designer.cs
@@ -1529,6 +1529,15 @@ public static string GameLauncherPage_PleaseRestartAsAdministrator {
}
}
+ ///
+ /// 查找类似 Relocate 的本地化字符串。
+ ///
+ public static string GameLauncherPage_Relocate {
+ get {
+ return ResourceManager.GetString("GameLauncherPage_Relocate", resourceCulture);
+ }
+ }
+
///
/// 查找类似 Removable storage device not connected. 的本地化字符串。
///
@@ -4072,6 +4081,24 @@ public static string StarRailGachaService_ImportWarpRecordsSuccessfully {
}
}
+ ///
+ /// 查找类似 Resume Download 的本地化字符串。
+ ///
+ public static string StartGameButton_ResumeDownload {
+ get {
+ return ResourceManager.GetString("StartGameButton_ResumeDownload", resourceCulture);
+ }
+ }
+
+ ///
+ /// 查找类似 Waiting 的本地化字符串。
+ ///
+ public static string StartGameButton_Waiting {
+ get {
+ return ResourceManager.GetString("StartGameButton_Waiting", resourceCulture);
+ }
+ }
+
///
/// 查找类似 This feature can quickly switch the client to the corresponding server, only needing to download some resources during the first switch. 的本地化字符串。
///
diff --git a/src/Starward.Language/Lang.resx b/src/Starward.Language/Lang.resx
index 9c8f9e9ed..3c47d3062 100644
--- a/src/Starward.Language/Lang.resx
+++ b/src/Starward.Language/Lang.resx
@@ -1609,4 +1609,13 @@ Do you accept the risk and continue to use it?
Other Rewards
+
+ Resume Download
+
+
+ Waiting
+
+
+ Relocate
+
\ No newline at end of file
diff --git a/src/Starward.Language/Lang.zh-CN.resx b/src/Starward.Language/Lang.zh-CN.resx
index 3687f014f..a668f91d6 100644
--- a/src/Starward.Language/Lang.zh-CN.resx
+++ b/src/Starward.Language/Lang.zh-CN.resx
@@ -59,46 +59,46 @@
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
-
-
+
+
-
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
+
-
+
@@ -1580,7 +1580,7 @@
辉彩祝福
- 可移动存储设备未连接
+ 可移动存储设备未连接。
菲林收入组成
@@ -1609,4 +1609,13 @@
其他奖励
+
+ 继续下载
+
+
+ 等待中
+
+
+ 重新定位
+
\ No newline at end of file
diff --git a/src/Starward.Language/Lang.zh-TW.resx b/src/Starward.Language/Lang.zh-TW.resx
index 84ce46146..e73fcf67a 100644
--- a/src/Starward.Language/Lang.zh-TW.resx
+++ b/src/Starward.Language/Lang.zh-TW.resx
@@ -60,45 +60,45 @@
: and then encoded with base64 encoding.
-->
-
+
-
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
+
+
-
-
-
-
+
+
+
+
-
+
-
+
@@ -1580,7 +1580,7 @@
輝彩祝福
- 可移動存儲設備未連接
+ 可移動存儲設備未連接。
Polychrome Revenue Streams
diff --git a/src/Starward/Features/Background/AppBackground.xaml b/src/Starward/Features/Background/AppBackground.xaml
index 6e7d85f64..d52414e65 100644
--- a/src/Starward/Features/Background/AppBackground.xaml
+++ b/src/Starward/Features/Background/AppBackground.xaml
@@ -43,6 +43,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Starward/Features/GameLauncher/GameLauncherPage.xaml.cs b/src/Starward/Features/GameLauncher/GameLauncherPage.xaml.cs
index 5f5073f1a..28640781c 100644
--- a/src/Starward/Features/GameLauncher/GameLauncherPage.xaml.cs
+++ b/src/Starward/Features/GameLauncher/GameLauncherPage.xaml.cs
@@ -1,4 +1,9 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Microsoft.Extensions.Logging;
using Starward.Frameworks;
+using System;
+using System.Threading.Tasks;
namespace Starward.Features.GameLauncher;
@@ -7,10 +12,200 @@ public sealed partial class GameLauncherPage : PageBase
{
+ private readonly ILogger _logger = AppService.GetLogger();
+
+ private readonly GameLauncherService _gameLauncherService = AppService.GetService();
+
+ private readonly GamePackageService _gamePackageService = AppService.GetService();
+
+
public GameLauncherPage()
{
this.InitializeComponent();
}
+
+ [NotifyPropertyChangedFor(nameof(InstalledLocateGameEnabled))]
+ [ObservableProperty]
+ public partial GameState GameState { get; set; }
+
+
+ public bool StartGameButtonCanExecute { get; set => SetProperty(ref field, value); } = true;
+
+
+
+ protected override void OnLoaded()
+ {
+ CheckGameVersion();
+ }
+
+
+
+
+
+ [RelayCommand]
+ private async Task ClickStartGameButtonAsync()
+ {
+ StartGameButtonCanExecute = false;
+ switch (GameState)
+ {
+ case GameState.StartGame:
+ await StartGameAsync();
+ break;
+ case GameState.GameIsRunning:
+ case GameState.InstallGame:
+ case GameState.UpdateGame:
+ case GameState.Downloading:
+ case GameState.Waiting:
+ case GameState.Paused:
+ case GameState.ResumeDownload:
+ default:
+ StartGameButtonCanExecute = true;
+ break;
+ }
+ }
+
+
+
+
+
+
+ #region Game Version
+
+
+ public string? GameInstallPath { get; set => SetProperty(ref field, value); }
+
+
+ public bool IsInstallPathRemovableTipEnabled { get; set => SetProperty(ref field, value); }
+
+
+ public bool InstalledLocateGameEnabled => GameState is GameState.InstallGame && !IsInstallPathRemovableTipEnabled;
+
+
+ private Version? localGameVersion;
+
+
+ private Version? latestGameVersion;
+
+
+ private Version? preInstallGameVersion;
+
+
+ private bool isGameExeExists;
+
+
+ private bool isPreDownloadOK;
+
+
+
+
+
+
+ private async void CheckGameVersion()
+ {
+ try
+ {
+ GameState = GameState.Waiting;
+ GameInstallPath = _gameLauncherService.GetGameInstallPath(CurrentGameId, out bool storageRemoved);
+ IsInstallPathRemovableTipEnabled = storageRemoved;
+ if (GameInstallPath is null || storageRemoved)
+ {
+ GameState = GameState.InstallGame;
+ return;
+ }
+ isGameExeExists = await _gameLauncherService.IsGameExeExistsAsync(CurrentGameId);
+ localGameVersion = await _gameLauncherService.GetLocalGameVersionAsync(CurrentGameId);
+ if (isGameExeExists && localGameVersion != null)
+ {
+ GameState = GameState.StartGame;
+ }
+ else
+ {
+ GameState = GameState.ResumeDownload;
+ return;
+ }
+ latestGameVersion = await _gamePackageService.GetLatestGameVersionAsync(CurrentGameId);
+ if (latestGameVersion > localGameVersion)
+ {
+ GameState = GameState.UpdateGame;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Check game version");
+ }
+ }
+
+
+
+
+ #endregion
+
+
+
+
+
+
+
+ #region Start Game
+
+
+
+
+
+
+
+
+ private async void UpdateGameState()
+ {
+ try
+ {
+
+ }
+ catch { }
+ }
+
+
+
+
+
+ [RelayCommand]
+ private async Task StartGameAsync()
+ {
+ try
+ {
+ await Task.Delay(2000);
+ var process1 = await _gameLauncherService.StartGameAsync(CurrentGameId);
+ if (process1 == null)
+ {
+ StartGameButtonCanExecute = true;
+ }
+ else
+ {
+ GameState = GameState.GameIsRunning;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Start game");
+ StartGameButtonCanExecute = true;
+ }
+ }
+
+
+
+
+ #endregion
+
+
+
+
+
+
+
+
+
+
+
}
diff --git a/src/Starward/Features/GameLauncher/GameLauncherService.cs b/src/Starward/Features/GameLauncher/GameLauncherService.cs
new file mode 100644
index 000000000..81977c6e7
--- /dev/null
+++ b/src/Starward/Features/GameLauncher/GameLauncherService.cs
@@ -0,0 +1,259 @@
+using Microsoft.Extensions.Logging;
+using Starward.Core;
+using Starward.Core.HoYoPlay;
+using Starward.Features.HoYoPlay;
+using Starward.Frameworks;
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace Starward.Features.GameLauncher;
+
+internal class GameLauncherService
+{
+
+
+ private readonly ILogger _logger;
+
+
+ private readonly HoYoPlayService _hoYoPlayService;
+
+
+
+
+ public GameLauncherService(ILogger logger, HoYoPlayService hoYoPlayService)
+ {
+ _logger = logger;
+ _hoYoPlayService = hoYoPlayService;
+ }
+
+
+
+
+
+ ///
+ /// 游戏安装目录,为空时未找到
+ ///
+ ///
+ ///
+ public string? GetGameInstallPath(GameId gameId)
+ {
+ var path = AppSetting.GetGameInstallPath(gameId.GameBiz);
+ if (Directory.Exists(path))
+ {
+ return Path.GetFullPath(path);
+ }
+ else if (!string.IsNullOrWhiteSpace(path) && AppSetting.GetGameInstallPathRemovable(gameId.GameBiz))
+ {
+ return path;
+ }
+ else
+ {
+ AppSetting.SetGameInstallPath(gameId.GameBiz, null);
+ AppSetting.SetGameInstallPathRemovable(gameId.GameBiz, false);
+ return null;
+ }
+ }
+
+
+
+ ///
+ /// 游戏安装目录,为空时未找到
+ ///
+ ///
+ /// 可移动存储设备已移除
+ ///
+ public string? GetGameInstallPath(GameId gameId, out bool storageRemoved)
+ {
+ storageRemoved = false;
+ var path = AppSetting.GetGameInstallPath(gameId.GameBiz);
+ if (Directory.Exists(path))
+ {
+ return Path.GetFullPath(path);
+ }
+ else if (!string.IsNullOrWhiteSpace(path) && AppSetting.GetGameInstallPathRemovable(gameId.GameBiz))
+ {
+ storageRemoved = true;
+ return path;
+ }
+ else
+ {
+ AppSetting.SetGameInstallPath(gameId.GameBiz, null);
+ AppSetting.SetGameInstallPathRemovable(gameId.GameBiz, false);
+ return null;
+ }
+ }
+
+
+
+
+
+ ///
+ /// 本地游戏版本
+ ///
+ ///
+ ///
+ public async Task GetLocalGameVersionAsync(GameId gameId, string? installPath = null)
+ {
+ installPath ??= GetGameInstallPath(gameId);
+ if (string.IsNullOrWhiteSpace(installPath))
+ {
+ return null;
+ }
+ var config = Path.Join(installPath, "config.ini");
+ if (File.Exists(config))
+ {
+ var str = await File.ReadAllTextAsync(config);
+ _ = Version.TryParse(Regex.Match(str, @"game_version=(.+)").Groups[1].Value, out Version? version);
+ return version;
+ }
+ else
+ {
+ _logger.LogWarning("config.ini not found: {path}", config);
+ return null;
+ }
+ }
+
+
+
+
+
+ ///
+ /// 游戏进程名,带 .exe 扩展名
+ ///
+ ///
+ ///
+ public async Task GetGameExeNameAsync(GameId gameId)
+ {
+ string? name = gameId.GameBiz.Value switch
+ {
+ GameBiz.hk4e_cn or GameBiz.hk4e_bilibili => "YuanShen.exe",
+ GameBiz.hk4e_global => "GenshinImpact.exe",
+ _ => gameId.GameBiz.Game switch
+ {
+ GameBiz.hkrpg => "StarRail.exe",
+ GameBiz.bh3 => "BH3.exe",
+ GameBiz.nap => "ZenlessZoneZero.exe",
+ _ => null,
+ },
+ };
+ if (string.IsNullOrWhiteSpace(name))
+ {
+ var config = await _hoYoPlayService.GetGameConfigAsync(gameId);
+ name = config?.ExeFileName;
+ }
+ return name ?? throw new ArgumentOutOfRangeException($"Unknown game ({gameId.Id}, {gameId.GameBiz}).");
+ }
+
+
+
+ ///
+ /// 游戏进程文件是否存在
+ ///
+ ///
+ ///
+ ///
+ public async Task IsGameExeExistsAsync(GameId gameId, string? installPath = null)
+ {
+ installPath ??= GetGameInstallPath(gameId);
+ if (!string.IsNullOrWhiteSpace(installPath))
+ {
+ var exe = Path.Join(installPath, await GetGameExeNameAsync(gameId));
+ return File.Exists(exe);
+ }
+ return false;
+ }
+
+
+
+ ///
+ /// 获取游戏进程
+ ///
+ ///
+ ///
+ public async Task GetGameProcessAsync(GameId gameId)
+ {
+ int currentSessionId = Process.GetCurrentProcess().SessionId;
+ var name = (await GetGameExeNameAsync(gameId)).Replace(".exe", "");
+ return Process.GetProcessesByName(name).Where(x => x.SessionId == currentSessionId).FirstOrDefault();
+ }
+
+
+
+ ///
+ /// 启动游戏
+ ///
+ ///
+ public async Task StartGameAsync(GameId gameId, bool ignoreRunningGame = false, string? installPath = null)
+ {
+ const int ERROR_CANCELLED = 0x000004C7;
+ try
+ {
+ if (!ignoreRunningGame)
+ {
+ if (await GetGameProcessAsync(gameId) != null)
+ {
+ throw new Exception("Game process is running.");
+ }
+ }
+ string? exe = null, arg = null, verb = null;
+ if (Directory.Exists(installPath))
+ {
+ var e = Path.Join(installPath, await GetGameExeNameAsync(gameId));
+ if (File.Exists(e))
+ {
+ exe = e;
+ }
+ }
+ if (string.IsNullOrWhiteSpace(exe) && AppSetting.GetEnableThirdPartyTool(gameId.GameBiz))
+ {
+ exe = AppSetting.GetThirdPartyToolPath(gameId.GameBiz);
+ if (File.Exists(exe))
+ {
+ verb = Path.GetExtension(exe) is ".exe" or ".bat" ? "runas" : "";
+ }
+ else
+ {
+ exe = null;
+ AppSetting.SetThirdPartyToolPath(gameId.GameBiz, null);
+ _logger.LogWarning("Third party tool not found: {path}", exe);
+ }
+ }
+ if (string.IsNullOrWhiteSpace(exe))
+ {
+ var folder = GetGameInstallPath(gameId);
+ var name = await GetGameExeNameAsync(gameId);
+ exe = Path.Join(folder, name);
+ arg = AppSetting.GetStartArgument(gameId.GameBiz)?.Trim();
+ verb = "runas";
+ if (!File.Exists(exe))
+ {
+ _logger.LogWarning("Game exe not found: {path}", exe);
+ throw new FileNotFoundException("Game exe not found", name);
+ }
+ }
+ _logger.LogInformation("Start game ({biz})\r\npath: {exe}\r\narg: {arg}", gameId, exe, arg);
+ var info = new ProcessStartInfo
+ {
+ FileName = exe,
+ Arguments = arg,
+ UseShellExecute = true,
+ Verb = verb,
+ WorkingDirectory = Path.GetDirectoryName(exe),
+ };
+ return Process.Start(info);
+ }
+ catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_CANCELLED)
+ {
+ // Operation canceled
+ _logger.LogInformation("Start game operation canceled.");
+ }
+ return null;
+ }
+
+
+}
diff --git a/src/Starward/Features/GameLauncher/GamePackageService.cs b/src/Starward/Features/GameLauncher/GamePackageService.cs
new file mode 100644
index 000000000..4c9d576cd
--- /dev/null
+++ b/src/Starward/Features/GameLauncher/GamePackageService.cs
@@ -0,0 +1,263 @@
+using Microsoft.Extensions.Logging;
+using Starward.Core;
+using Starward.Core.HoYoPlay;
+using Starward.Features.HoYoPlay;
+using Starward.Models;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Starward.Features.GameLauncher;
+
+internal class GamePackageService
+{
+
+
+ private readonly ILogger _logger;
+
+ private readonly HoYoPlayService _hoYoPlayService;
+
+ private readonly GameLauncherService _gameLauncherService;
+
+
+
+ public GamePackageService(ILogger logger, HoYoPlayService hoYoPlayService, GameLauncherService gameLauncherService)
+ {
+ _logger = logger;
+ _hoYoPlayService = hoYoPlayService;
+ _gameLauncherService = gameLauncherService;
+ }
+
+
+
+
+
+ public async Task GetGamePackageAsync(GameId gameId)
+ {
+ return await _hoYoPlayService.GetGamePackageAsync(gameId);
+ }
+
+
+
+ ///
+ /// 最新游戏版本
+ ///
+ ///
+ ///
+ public async Task GetLatestGameVersionAsync(GameId gameId)
+ {
+ var package = await _hoYoPlayService.GetGamePackageAsync(gameId);
+ _ = Version.TryParse(package.Main.Major?.Version, out Version? version);
+ return version;
+ }
+
+
+
+
+
+ public async Task CheckPreDownloadIsOKAsync(GameId gameId, string? installPath = null)
+ {
+ installPath ??= _gameLauncherService.GetGameInstallPath(gameId);
+ if (string.IsNullOrWhiteSpace(installPath))
+ {
+ return false;
+ }
+ var package = await GetGamePackageAsync(gameId);
+ if (package.PreDownload?.Major != null)
+ {
+ var localVersion = await _gameLauncherService.GetLocalGameVersionAsync(gameId, installPath);
+ VoiceLanguage language = await GetVoiceLanguageAsync(gameId, installPath);
+ if (package.PreDownload.Patches?.FirstOrDefault(x => x.Version == localVersion?.ToString()) is GamePackageResource resource)
+ {
+ return CheckGamePackageResourceIsDownloadOK(resource, installPath, language);
+ }
+ else
+ {
+ return CheckGamePackageResourceIsDownloadOK(package.PreDownload.Major, installPath, language);
+ }
+ }
+ return false;
+ }
+
+
+
+
+
+ public async Task GetNeedDownloadGamePackageResourceAsync(GameId gameId, string? installPath = null)
+ {
+ installPath ??= _gameLauncherService.GetGameInstallPath(gameId);
+ Version? localVersion = await _gameLauncherService.GetLocalGameVersionAsync(gameId, installPath);
+ Version? latestVersion = await GetLatestGameVersionAsync(gameId);
+ if (latestVersion is null)
+ {
+ return null;
+ }
+ GamePackage package = await GetGamePackageAsync(gameId);
+ if (localVersion is null)
+ {
+ return package.Main.Major;
+ }
+ else if (localVersion < latestVersion)
+ {
+ if (package.Main.Patches.FirstOrDefault(x => x.Version == localVersion.ToString()) is GamePackageResource resource)
+ {
+ return resource;
+ }
+ else
+ {
+ return package.Main.Major;
+ }
+ }
+ else if (package.PreDownload is not null)
+ {
+ if (package.PreDownload.Patches.FirstOrDefault(x => x.Version == localVersion.ToString()) is GamePackageResource resource)
+ {
+ return resource;
+ }
+ else
+ {
+ return package.PreDownload.Major;
+ }
+ }
+ return null;
+ }
+
+
+
+
+
+ private static bool CheckGamePackageResourceIsDownloadOK(GamePackageResource resource, string installPath, VoiceLanguage language)
+ {
+ foreach (var item in resource.GamePackages)
+ {
+ string file = Path.Combine(installPath, Path.GetFileName(item.Url));
+ if (!File.Exists(file))
+ {
+ return false;
+ }
+ }
+ foreach (var lang in Enum.GetValues())
+ {
+ if (language.HasFlag(lang))
+ {
+ if (resource.AudioPackages.FirstOrDefault(x => x.Language == lang.ToDescription()) is GamePackageFile packageFile)
+ {
+ string file = Path.Combine(installPath, Path.GetFileName(packageFile.Url));
+ if (!File.Exists(file))
+ {
+ return false;
+ }
+ }
+ }
+ }
+ return true;
+ }
+
+
+
+
+ private static long GetFileDownloadedLength(string file)
+ {
+ if (File.Exists(file))
+ {
+ return new FileInfo(file).Length;
+ }
+ else if (File.Exists(file + "_tmp"))
+ {
+ return new FileInfo(file + "_tmp").Length;
+ }
+ return 0;
+ }
+
+
+
+
+ public async Task GetVoiceLanguageAsync(GameId gameId, string? installPath = null)
+ {
+ if (gameId.GameBiz.Game == GameBiz.bh3)
+ {
+ return VoiceLanguage.None;
+ }
+ installPath ??= _gameLauncherService.GetGameInstallPath(gameId);
+ if (string.IsNullOrWhiteSpace(installPath))
+ {
+ return VoiceLanguage.None;
+ }
+ VoiceLanguage flag = VoiceLanguage.None;
+ var config = await _hoYoPlayService.GetGameConfigAsync(gameId);
+ if (!string.IsNullOrWhiteSpace(config?.AudioPackageScanDir))
+ {
+ string file = Path.Join(installPath, config.AudioPackageScanDir);
+ if (File.Exists(file))
+ {
+ var lines = await File.ReadAllLinesAsync(file);
+ if (lines.Any(x => x.Contains("Chinese"))) { flag |= VoiceLanguage.Chinese; }
+ if (lines.Any(x => x.Contains("English(US)"))) { flag |= VoiceLanguage.English; }
+ if (lines.Any(x => x.Contains("Japanese"))) { flag |= VoiceLanguage.Japanese; }
+ if (lines.Any(x => x.Contains("Korean"))) { flag |= VoiceLanguage.Korean; }
+ }
+ }
+ return flag;
+ }
+
+
+
+
+ public async Task SetVoiceLanguageAsync(GameId gameId, string installPath, VoiceLanguage lang)
+ {
+ if (gameId.GameBiz.Game == GameBiz.bh3)
+ {
+ return;
+ }
+ var config = await _hoYoPlayService.GetGameConfigAsync(gameId);
+ if (!string.IsNullOrWhiteSpace(config?.AudioPackageScanDir))
+ {
+ string file = Path.Join(installPath, config.AudioPackageScanDir);
+ Directory.CreateDirectory(Path.GetDirectoryName(file)!);
+ var lines = new List(4);
+ if (lang.HasFlag(VoiceLanguage.Chinese)) { lines.Add("Chinese"); }
+ if (lang.HasFlag(VoiceLanguage.English)) { lines.Add("English(US)"); }
+ if (lang.HasFlag(VoiceLanguage.Japanese)) { lines.Add("Japanese"); }
+ if (lang.HasFlag(VoiceLanguage.Korean)) { lines.Add("Korean"); }
+ await File.WriteAllLinesAsync(file, lines);
+ }
+ }
+
+
+
+
+ public DownloadGameResource GetDownloadGameResourceAsync(GamePackageResource resource, string installPath)
+ {
+ var downloadResource = new DownloadGameResource
+ {
+ Game = new DownloadPackageState
+ {
+ Name = resource.Version,
+ Url = resource.GamePackages[0].Url,
+ PackageSize = resource.GamePackages.Sum(x => x.Size),
+ DecompressedSize = resource.GamePackages.Sum(x => x.DecompressedSize),
+ DownloadedSize = resource.GamePackages.Sum(x => GetFileDownloadedLength(Path.Combine(installPath, Path.GetFileName(x.Url)))),
+ },
+ FreeSpace = new DriveInfo(Path.GetFullPath(installPath)).AvailableFreeSpace,
+ };
+ foreach (var item in resource.AudioPackages)
+ {
+ downloadResource.Voices.Add(new DownloadPackageState
+ {
+ Name = item.Language!,
+ Url = item.Url,
+ PackageSize = item.Size,
+ DecompressedSize = item.DecompressedSize,
+ DownloadedSize = GetFileDownloadedLength(Path.Combine(installPath, Path.GetFileName(item.Url))),
+ });
+ }
+ return downloadResource;
+ }
+
+
+
+
+
+}
diff --git a/src/Starward/Features/GameLauncher/GameState.cs b/src/Starward/Features/GameLauncher/GameState.cs
new file mode 100644
index 000000000..0b6a2e865
--- /dev/null
+++ b/src/Starward/Features/GameLauncher/GameState.cs
@@ -0,0 +1,32 @@
+namespace Starward.Features.GameLauncher;
+
+public enum GameState
+{
+
+ None,
+
+
+ StartGame,
+
+
+ GameIsRunning,
+
+
+ InstallGame,
+
+
+ UpdateGame,
+
+
+ Downloading,
+
+
+ Waiting,
+
+
+ Paused,
+
+
+ ResumeDownload,
+
+}
\ No newline at end of file
diff --git a/src/Starward/Features/GameLauncher/StartGameButton.xaml b/src/Starward/Features/GameLauncher/StartGameButton.xaml
new file mode 100644
index 000000000..442148eee
--- /dev/null
+++ b/src/Starward/Features/GameLauncher/StartGameButton.xaml
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Starward/Features/GameLauncher/StartGameButton.xaml.cs b/src/Starward/Features/GameLauncher/StartGameButton.xaml.cs
new file mode 100644
index 000000000..dd520f28c
--- /dev/null
+++ b/src/Starward/Features/GameLauncher/StartGameButton.xaml.cs
@@ -0,0 +1,152 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using System.Windows.Input;
+
+
+namespace Starward.Features.GameLauncher;
+
+[INotifyPropertyChanged]
+public sealed partial class StartGameButton : UserControl
+{
+
+
+ private static Brush AccentFillColorDefaultBrush => (Brush)Application.Current.Resources["AccentFillColorDefaultBrush"];
+ private static Brush TextOnAccentFillColorDisabled => (Brush)Application.Current.Resources["TextOnAccentFillColorDisabledBrush"];
+ private static Brush TextOnAccentFillColorPrimaryBrush => (Brush)Application.Current.Resources["TextOnAccentFillColorPrimaryBrush"];
+
+
+ public StartGameButton()
+ {
+ this.InitializeComponent();
+ this.ActualThemeChanged += StartGameButton_ActualThemeChanged;
+ }
+
+
+
+ public GameState State
+ {
+ get;
+ set
+ {
+ SetProperty(ref field, value);
+ UpdateButtonState();
+ }
+ }
+
+
+ public bool PointerOver
+ {
+ get;
+ set
+ {
+ SetProperty(ref field, value);
+ UpdateButtonState();
+ }
+ }
+
+
+ public bool CanExecute
+ {
+ get;
+ set
+ {
+ SetProperty(ref field, value);
+ UpdateButtonState();
+ }
+ } = true;
+
+
+
+
+ public bool TextBlock_StartGame_Visibility => State is GameState.StartGame;
+ public bool TextBlock_GameIsRunning_Visibility => State is GameState.GameIsRunning;
+ public bool TextBlock_InstallGame_Visibility => State is GameState.InstallGame;
+ public bool TextBlock_UpdateGame_Visibility => State is GameState.UpdateGame;
+ public bool TextBlock_ResumeDownload_Visibility => State is GameState.ResumeDownload or GameState.Paused;
+ public double TextBlock_ResumeDownload_Opacity => (State is GameState.ResumeDownload || PointerOver) ? 1 : 0;
+ public bool TextBlock_Waiting_Visibility => State is GameState.Waiting;
+ public bool TextBlock_Pause_Visibility => State is GameState.Downloading && PointerOver;
+ public double TextBlock_Pause_Opacity => TextBlock_Pause_Visibility ? 1 : 0;
+ public bool TextBlock_Paused_Visibility => State is GameState.Paused && !PointerOver;
+
+
+ public bool Rect_AccentBackground_Visibility => CanExecute && !(State is GameState.GameIsRunning or GameState.Downloading or GameState.Paused);
+
+ public bool ProgressRing_IndeterminateLoading_Visibility => (State is GameState.StartGame or GameState.InstallGame or GameState.UpdateGame or GameState.ResumeDownload or GameState.Paused) && !CanExecute;
+
+
+
+
+ public ICommand Command { get; set => SetProperty(ref field, value); }
+
+
+ public ICommand SettingCommand { get; set => SetProperty(ref field, value); }
+
+
+
+
+
+ private void UpdateButtonState()
+ {
+ OnPropertyChanged(nameof(TextBlock_StartGame_Visibility));
+ OnPropertyChanged(nameof(TextBlock_StartGame_Visibility));
+ OnPropertyChanged(nameof(TextBlock_GameIsRunning_Visibility));
+ OnPropertyChanged(nameof(TextBlock_InstallGame_Visibility));
+ OnPropertyChanged(nameof(TextBlock_UpdateGame_Visibility));
+ OnPropertyChanged(nameof(TextBlock_ResumeDownload_Visibility));
+ OnPropertyChanged(nameof(TextBlock_ResumeDownload_Opacity));
+ OnPropertyChanged(nameof(TextBlock_Waiting_Visibility));
+ OnPropertyChanged(nameof(TextBlock_Pause_Visibility));
+ OnPropertyChanged(nameof(TextBlock_Pause_Opacity));
+ OnPropertyChanged(nameof(TextBlock_Paused_Visibility));
+ OnPropertyChanged(nameof(Rect_AccentBackground_Visibility));
+ OnPropertyChanged(nameof(ProgressRing_IndeterminateLoading_Visibility));
+
+ Button_GameAction.Foreground = (CanExecute, Rect_AccentBackground_Visibility, PointerOver) switch
+ {
+ (false, _, _) => TextOnAccentFillColorDisabled,
+ (true, false, true) => AccentFillColorDefaultBrush,
+ (true, false, false) => TextOnAccentFillColorDisabled,
+ _ => TextOnAccentFillColorPrimaryBrush
+ };
+ Button_Setting.Foreground = (Rect_AccentBackground_Visibility, PointerOver) switch
+ {
+ (false, true) => AccentFillColorDefaultBrush,
+ (false, false) => TextOnAccentFillColorDisabled,
+ _ => TextOnAccentFillColorPrimaryBrush
+ };
+ }
+
+
+
+ private void Grid_Root_PointerEntered(object sender, PointerRoutedEventArgs e)
+ {
+ PointerOver = true;
+ if (State is GameState.Downloading)
+ {
+ FlyoutBase.ShowAttachedFlyout(Grid_Root);
+ }
+ }
+
+
+
+ private void Grid_Root_PointerExited(object sender, PointerRoutedEventArgs e)
+ {
+ PointerOver = false;
+ Flyout_DownloadProgress.Hide();
+ }
+
+
+
+ private void StartGameButton_ActualThemeChanged(FrameworkElement sender, object args)
+ {
+ UpdateButtonState();
+ }
+
+
+
+}
diff --git a/src/Starward/Features/GameSelector/GameSelector.xaml b/src/Starward/Features/GameSelector/GameSelector.xaml
index 6b13c5ec2..aca916c6e 100644
--- a/src/Starward/Features/GameSelector/GameSelector.xaml
+++ b/src/Starward/Features/GameSelector/GameSelector.xaml
@@ -97,6 +97,7 @@
@@ -105,9 +106,12 @@
VerticalAlignment="Center"
Background="#60000000"
Source="{x:Bind GameInfo.Display.Logo.Url}" />
-
+
diff --git a/src/Starward/Frameworks/PageBase.cs b/src/Starward/Frameworks/PageBase.cs
index ea900b1a6..c753450ae 100644
--- a/src/Starward/Frameworks/PageBase.cs
+++ b/src/Starward/Frameworks/PageBase.cs
@@ -11,7 +11,7 @@ public abstract partial class PageBase : Page
{
- public GameId? CurrentGameId { get; protected set => SetProperty(ref field, value); }
+ public GameId CurrentGameId { get; protected set => SetProperty(ref field, value); }
public GameBiz CurrentGameBiz { get; protected set => SetProperty(ref field, value); }
@@ -51,7 +51,7 @@ protected override void OnNavigatedTo(NavigationEventArgs e)
else if (e.Parameter is GameBiz biz)
{
CurrentGameBiz = biz;
- CurrentGameId = GameId.FromGameBiz(biz);
+ CurrentGameId = GameId.FromGameBiz(biz)!;
}
}