From 72c363e3b38801f42ab55f53e20091ce56ac35e4 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Wed, 7 Jan 2026 20:56:43 +0100 Subject: [PATCH 01/46] Cherry-pick Launcher from xplat-editor branch --- build/Stride.Launcher.sln | 76 +- sources/launcher/Stride.Launcher/App.axaml | 61 ++ sources/launcher/Stride.Launcher/App.axaml.cs | 70 ++ sources/launcher/Stride.Launcher/App.xaml | 20 - sources/launcher/Stride.Launcher/App.xaml.cs | 11 - .../Assets/CrashReportImage.png | 3 + .../Images}/EditorIcon.png | 0 .../chat-16.png => Assets/Images/chat.png} | 0 .../Images/delete.png} | 0 .../{Resources => Assets/Images}/discord.png | 0 .../Images/download.png} | 0 .../Images/facebook.png} | 0 .../Images}/getting-started.png | 0 .../{Resources => Assets/Images}/github.png | 0 .../{Resources => Assets/Images}/issues.png | 0 .../list-26.png => Assets/Images/list.png} | 0 .../{Resources => Assets/Images}/news.png | 0 .../Images/note.png} | 0 .../Images/opencollective.png} | 0 .../Images}/recent-projects.png | 0 .../Images/reddit.png} | 0 .../{Resources => Assets/Images}/roadmap.png | 0 .../Stride.Launcher/Assets/Images/robot.png | 3 + .../{Resources => Assets/Images}/showcase.png | 0 .../{Resources => Assets/Images}/survey.png | 0 .../Images}/switch-version.png | 0 .../Images/twitch.png} | 0 .../{Resources => Assets/Images}/update.png | 0 .../Images/upgrade.png} | 0 .../Images}/visual-studio.png | 0 .../Images/xtwitter.png} | 0 .../{Resources => Assets}/Launcher.ico | Bin .../Localization}/Strings.Designer.cs | 8 +- .../Localization}/Strings.ja-JP.resx | 0 .../Localization}/Strings.resx | 2 +- .../Localization}/Urls.Designer.cs | 8 +- .../Localization}/Urls.ja-JP.resx | 0 .../Localization}/Urls.resx | 4 +- sources/launcher/Stride.Launcher/Constants.cs | 18 + .../Stride.Launcher/Crash/CrashReportArgs.cs | 18 + .../Stride.Launcher/Crash/CrashReportData.cs | 52 ++ .../Crash/CrashReportViewModel.cs | 118 +++ .../Crash/CrashReportWindow.axaml | 47 + .../Crash/CrashReportWindow.axaml.cs | 23 + .../CrashReport/CrashReportHelper.cs | 70 -- sources/launcher/Stride.Launcher/Launcher.cs | 347 ++++---- .../Stride.Launcher/LauncherArguments.cs | 35 +- .../Stride.Launcher/LauncherErrorCode.cs | 37 +- .../Stride.Launcher/LauncherInstance.cs | 152 ---- .../PackageFilterExtensions.cs | 22 +- .../Stride.Launcher/PrerequisitesValidator.cs | 111 --- sources/launcher/Stride.Launcher/Program.cs | 60 +- .../Properties/AssemblyInfo.cs | 14 - .../PublishProfiles/FolderProfile.pubxml | 6 +- .../Stride.Launcher/Resources/Robot.jpg | 3 - .../Services/GameStudioSettings.cs | 206 +++-- .../Services/LauncherSettings.cs | 103 ++- .../Stride.Launcher/Services/MetricsHelper.cs | 37 - .../Stride.Launcher/Services/SelfUpdater.cs | 400 +++++---- .../Services/UninstallHelper.cs | 214 +++-- .../Stride.Launcher/Stride.Launcher.csproj | 142 +-- .../Stride.Launcher/Stride.Launcher.nuspec | 2 +- .../ViewModels/AnnouncementViewModel.cs | 96 +-- .../ViewModels/DocumentationPageViewModel.cs | 203 +++-- .../ViewModels/FrameworkConverter.cs | 37 +- .../ViewModels/LauncherViewModel.cs | 682 --------------- .../ViewModels/MainViewModel.cs | 699 +++++++++++++++ .../ViewModels/NewsPageViewModel.cs | 158 ++-- .../ViewModels/PackageVersionViewModel.cs | 512 ++++++----- .../ViewModels/RecentProjectViewModel.cs | 185 ++-- .../ViewModels/ReleaseNotesViewModel.cs | 173 ++-- .../ViewModels/StrideDevVersionViewModel.cs | 137 ++- .../StrideStoreAlternateVersionViewModel.cs | 94 +- .../ViewModels/StrideStoreVersionViewModel.cs | 467 +++++----- .../ViewModels/StrideVersionViewModel.cs | 368 ++++---- .../ViewModels/VsixVersionViewModel.cs | 228 +++-- .../Stride.Launcher/Views/Announcement.axaml | 22 + .../Views/Announcement.axaml.cs | 14 + .../Stride.Launcher/Views/Announcement.xaml | 60 -- .../Views/Announcement.xaml.cs | 17 - .../Stride.Launcher/Views/Commands.cs | 53 -- .../Stride.Launcher/Views/LauncherWindow.xaml | 808 ------------------ .../Views/LauncherWindow.xaml.cs | 122 --- .../Stride.Launcher/Views/MainView.axaml | 713 ++++++++++++++++ .../Stride.Launcher/Views/MainView.axaml.cs | 35 + .../Stride.Launcher/Views/MainWindow.axaml | 13 + .../Stride.Launcher/Views/MainWindow.axaml.cs | 14 + .../ProgressToIndeterminatedConverter.cs | 15 +- .../Views/ProgressToRectConverter.cs | 25 - .../Views/SelfUpdateWindow.axaml | 24 + .../Views/SelfUpdateWindow.axaml.cs | 54 ++ .../Views/SelfUpdateWindow.xaml | 14 - .../Views/SelfUpdateWindow.xaml.cs | 47 - .../Views/StaysOpenContextMenu.cs | 49 -- sources/launcher/Stride.Launcher/app.manifest | 45 +- 95 files changed, 4154 insertions(+), 4532 deletions(-) create mode 100644 sources/launcher/Stride.Launcher/App.axaml create mode 100644 sources/launcher/Stride.Launcher/App.axaml.cs delete mode 100644 sources/launcher/Stride.Launcher/App.xaml delete mode 100644 sources/launcher/Stride.Launcher/App.xaml.cs create mode 100644 sources/launcher/Stride.Launcher/Assets/CrashReportImage.png rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/EditorIcon.png (100%) rename sources/launcher/Stride.Launcher/{Resources/chat-16.png => Assets/Images/chat.png} (100%) rename sources/launcher/Stride.Launcher/{Resources/delete-26-dark.png => Assets/Images/delete.png} (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/discord.png (100%) rename sources/launcher/Stride.Launcher/{Resources/download-26-dark.png => Assets/Images/download.png} (100%) rename sources/launcher/Stride.Launcher/{Resources/facebook_24.png => Assets/Images/facebook.png} (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/getting-started.png (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/github.png (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/issues.png (100%) rename sources/launcher/Stride.Launcher/{Resources/list-26.png => Assets/Images/list.png} (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/news.png (100%) rename sources/launcher/Stride.Launcher/{Resources/note-26-dark.png => Assets/Images/note.png} (100%) rename sources/launcher/Stride.Launcher/{Resources/opencollective_24.png => Assets/Images/opencollective.png} (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/recent-projects.png (100%) rename sources/launcher/Stride.Launcher/{Resources/reddit_24.png => Assets/Images/reddit.png} (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/roadmap.png (100%) create mode 100644 sources/launcher/Stride.Launcher/Assets/Images/robot.png rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/showcase.png (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/survey.png (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/switch-version.png (100%) rename sources/launcher/Stride.Launcher/{Resources/twitch_24.png => Assets/Images/twitch.png} (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/update.png (100%) rename sources/launcher/Stride.Launcher/{Resources/upgrade-16.png => Assets/Images/upgrade.png} (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Images}/visual-studio.png (100%) rename sources/launcher/Stride.Launcher/{Resources/xtwitter_24.png => Assets/Images/xtwitter.png} (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets}/Launcher.ico (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Localization}/Strings.Designer.cs (99%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Localization}/Strings.ja-JP.resx (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Localization}/Strings.resx (99%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Localization}/Urls.Designer.cs (97%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Localization}/Urls.ja-JP.resx (100%) rename sources/launcher/Stride.Launcher/{Resources => Assets/Localization}/Urls.resx (99%) create mode 100644 sources/launcher/Stride.Launcher/Constants.cs create mode 100644 sources/launcher/Stride.Launcher/Crash/CrashReportArgs.cs create mode 100644 sources/launcher/Stride.Launcher/Crash/CrashReportData.cs create mode 100644 sources/launcher/Stride.Launcher/Crash/CrashReportViewModel.cs create mode 100644 sources/launcher/Stride.Launcher/Crash/CrashReportWindow.axaml create mode 100644 sources/launcher/Stride.Launcher/Crash/CrashReportWindow.axaml.cs delete mode 100644 sources/launcher/Stride.Launcher/CrashReport/CrashReportHelper.cs delete mode 100644 sources/launcher/Stride.Launcher/LauncherInstance.cs delete mode 100644 sources/launcher/Stride.Launcher/PrerequisitesValidator.cs delete mode 100644 sources/launcher/Stride.Launcher/Properties/AssemblyInfo.cs delete mode 100644 sources/launcher/Stride.Launcher/Resources/Robot.jpg delete mode 100644 sources/launcher/Stride.Launcher/Services/MetricsHelper.cs delete mode 100644 sources/launcher/Stride.Launcher/ViewModels/LauncherViewModel.cs create mode 100644 sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs create mode 100644 sources/launcher/Stride.Launcher/Views/Announcement.axaml create mode 100644 sources/launcher/Stride.Launcher/Views/Announcement.axaml.cs delete mode 100644 sources/launcher/Stride.Launcher/Views/Announcement.xaml delete mode 100644 sources/launcher/Stride.Launcher/Views/Announcement.xaml.cs delete mode 100644 sources/launcher/Stride.Launcher/Views/Commands.cs delete mode 100644 sources/launcher/Stride.Launcher/Views/LauncherWindow.xaml delete mode 100644 sources/launcher/Stride.Launcher/Views/LauncherWindow.xaml.cs create mode 100644 sources/launcher/Stride.Launcher/Views/MainView.axaml create mode 100644 sources/launcher/Stride.Launcher/Views/MainView.axaml.cs create mode 100644 sources/launcher/Stride.Launcher/Views/MainWindow.axaml create mode 100644 sources/launcher/Stride.Launcher/Views/MainWindow.axaml.cs delete mode 100644 sources/launcher/Stride.Launcher/Views/ProgressToRectConverter.cs create mode 100644 sources/launcher/Stride.Launcher/Views/SelfUpdateWindow.axaml create mode 100644 sources/launcher/Stride.Launcher/Views/SelfUpdateWindow.axaml.cs delete mode 100644 sources/launcher/Stride.Launcher/Views/SelfUpdateWindow.xaml delete mode 100644 sources/launcher/Stride.Launcher/Views/SelfUpdateWindow.xaml.cs delete mode 100644 sources/launcher/Stride.Launcher/Views/StaysOpenContextMenu.cs diff --git a/build/Stride.Launcher.sln b/build/Stride.Launcher.sln index 9c3d48561b..1f11872f81 100644 --- a/build/Stride.Launcher.sln +++ b/build/Stride.Launcher.sln @@ -1,9 +1,9 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.28803.352 +# Visual Studio Version 18 +VisualStudioVersion = 18.1.11312.151 MinimumVisualStudioVersion = 16.0 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Launcher", "..\sources\launcher\Stride.Launcher\Stride.Launcher.csproj", "{0F8BE30E-C41F-4747-B52B-D2D4E13EC6A2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Launcher", "..\sources\launcher\Stride.Launcher\Stride.Launcher.csproj", "{78695DE1-E621-45DF-975D-CFD407081E23}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core", "..\sources\core\Stride.Core\Stride.Core.csproj", "{BAC8FB10-95ED-4FBB-9925-F6F0E15BD936}" EndProject @@ -21,35 +21,32 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.IO", "..\source EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.MicroThreading", "..\sources\core\Stride.Core.MicroThreading\Stride.Core.MicroThreading.csproj", "{076940AD-70F3-47A8-827C-8E722714F937}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.Presentation.Wpf", "..\sources\presentation\Stride.Core.Presentation.Wpf\Stride.Core.Presentation.Wpf.csproj", "{61E90191-22FF-4ADC-AFD0-FAB662589AB4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.Translation", "..\sources\core\Stride.Core.Translation\Stride.Core.Translation.csproj", "{8A23DF78-B3D6-41BD-BA50-19D0FBE4AB69}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.Presentation.Dialogs", "..\sources\presentation\Stride.Core.Presentation.Dialogs\Stride.Core.Presentation.Dialogs.csproj", "{5EB0493A-076D-4488-AF08-D812FB3FDF7C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.Packages", "..\sources\assets\Stride.Core.Packages\Stride.Core.Packages.csproj", "{1F5FBA04-C334-41C2-895A-ACC4B786F99E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.Translation", "..\sources\core\Stride.Core.Translation\Stride.Core.Translation.csproj", "{8A23DF78-B3D6-41BD-BA50-19D0FBE4AB69}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.Presentation", "..\sources\presentation\Stride.Core.Presentation\Stride.Core.Presentation.csproj", "{0C63EF8B-26F9-4511-9FC5-7431DE9657D6}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.Translation.Presentation", "..\sources\presentation\Stride.Core.Translation.Presentation\Stride.Core.Translation.Presentation.csproj", "{7B286D71-5143-4A08-B9DE-113B310A3F0C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.CompilerServices", "..\sources\core\Stride.Core.CompilerServices\Stride.Core.CompilerServices.csproj", "{ADE0E241-CBDD-48C3-8F50-98FFE76C03C8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.Packages", "..\sources\assets\Stride.Core.Packages\Stride.Core.Packages.csproj", "{1F5FBA04-C334-41C2-895A-ACC4B786F99E}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "10-CoreRuntime", "10-CoreRuntime", "{706260A8-86D4-432D-9FE0-F312863288F5}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "30-CoreDesign", "30-CoreDesign", "{FE721A31-DF09-4E33-B791-BEC6C9E1C6F1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stride.Core.Presentation", "..\sources\presentation\Stride.Core.Presentation\Stride.Core.Presentation.csproj", "{0C63EF8B-26F9-4511-9FC5-7431DE9657D6}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "50-Presentation", "50-Presentation", "{3BC606D7-27B3-41FA-8FB3-9D56AC8B4DD7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Stride.Editor.CrashReport", "..\sources\editor\Stride.Editor.CrashReport\Stride.Editor.CrashReport.csproj", "{2880C313-2483-416D-A902-DC2259EDFF64}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Stride.Core.Presentation.Avalonia", "..\sources\presentation\Stride.Core.Presentation.Avalonia\Stride.Core.Presentation.Avalonia.csproj", "{3B613E66-671E-4049-8EF5-43CDAA549210}" EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - ..\sources\assets\Stride.Core.Assets.Yaml\Stride.Core.Assets.Yaml.projitems*{0f8be30e-c41f-4747-b52b-d2d4e13ec6a2}*SharedItemsImports = 5 - ..\sources\editor\Stride.Core.MostRecentlyUsedFiles\Stride.Core.MostRecentlyUsedFiles.projitems*{0f8be30e-c41f-4747-b52b-d2d4e13ec6a2}*SharedItemsImports = 5 - ..\sources\editor\Stride.PrivacyPolicy\Stride.PrivacyPolicy.projitems*{0f8be30e-c41f-4747-b52b-d2d4e13ec6a2}*SharedItemsImports = 5 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0F8BE30E-C41F-4747-B52B-D2D4E13EC6A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0F8BE30E-C41F-4747-B52B-D2D4E13EC6A2}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0F8BE30E-C41F-4747-B52B-D2D4E13EC6A2}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0F8BE30E-C41F-4747-B52B-D2D4E13EC6A2}.Release|Any CPU.Build.0 = Release|Any CPU + {78695DE1-E621-45DF-975D-CFD407081E23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78695DE1-E621-45DF-975D-CFD407081E23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78695DE1-E621-45DF-975D-CFD407081E23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78695DE1-E621-45DF-975D-CFD407081E23}.Release|Any CPU.Build.0 = Release|Any CPU {BAC8FB10-95ED-4FBB-9925-F6F0E15BD936}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BAC8FB10-95ED-4FBB-9925-F6F0E15BD936}.Debug|Any CPU.Build.0 = Debug|Any CPU {BAC8FB10-95ED-4FBB-9925-F6F0E15BD936}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -82,22 +79,10 @@ Global {076940AD-70F3-47A8-827C-8E722714F937}.Debug|Any CPU.Build.0 = Debug|Any CPU {076940AD-70F3-47A8-827C-8E722714F937}.Release|Any CPU.ActiveCfg = Release|Any CPU {076940AD-70F3-47A8-827C-8E722714F937}.Release|Any CPU.Build.0 = Release|Any CPU - {61E90191-22FF-4ADC-AFD0-FAB662589AB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {61E90191-22FF-4ADC-AFD0-FAB662589AB4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {61E90191-22FF-4ADC-AFD0-FAB662589AB4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {61E90191-22FF-4ADC-AFD0-FAB662589AB4}.Release|Any CPU.Build.0 = Release|Any CPU - {5EB0493A-076D-4488-AF08-D812FB3FDF7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5EB0493A-076D-4488-AF08-D812FB3FDF7C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5EB0493A-076D-4488-AF08-D812FB3FDF7C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5EB0493A-076D-4488-AF08-D812FB3FDF7C}.Release|Any CPU.Build.0 = Release|Any CPU {8A23DF78-B3D6-41BD-BA50-19D0FBE4AB69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8A23DF78-B3D6-41BD-BA50-19D0FBE4AB69}.Debug|Any CPU.Build.0 = Debug|Any CPU {8A23DF78-B3D6-41BD-BA50-19D0FBE4AB69}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A23DF78-B3D6-41BD-BA50-19D0FBE4AB69}.Release|Any CPU.Build.0 = Release|Any CPU - {7B286D71-5143-4A08-B9DE-113B310A3F0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7B286D71-5143-4A08-B9DE-113B310A3F0C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7B286D71-5143-4A08-B9DE-113B310A3F0C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7B286D71-5143-4A08-B9DE-113B310A3F0C}.Release|Any CPU.Build.0 = Release|Any CPU {1F5FBA04-C334-41C2-895A-ACC4B786F99E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1F5FBA04-C334-41C2-895A-ACC4B786F99E}.Debug|Any CPU.Build.0 = Debug|Any CPU {1F5FBA04-C334-41C2-895A-ACC4B786F99E}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -106,15 +91,36 @@ Global {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C63EF8B-26F9-4511-9FC5-7431DE9657D6}.Release|Any CPU.Build.0 = Release|Any CPU - {2880C313-2483-416D-A902-DC2259EDFF64}.Debug|Any CPU.ActiveCfg = Debug|iPhone - {2880C313-2483-416D-A902-DC2259EDFF64}.Debug|Any CPU.Build.0 = Debug|iPhone - {2880C313-2483-416D-A902-DC2259EDFF64}.Release|Any CPU.ActiveCfg = Release|iPhone - {2880C313-2483-416D-A902-DC2259EDFF64}.Release|Any CPU.Build.0 = Release|iPhone + {ADE0E241-CBDD-48C3-8F50-98FFE76C03C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ADE0E241-CBDD-48C3-8F50-98FFE76C03C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ADE0E241-CBDD-48C3-8F50-98FFE76C03C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ADE0E241-CBDD-48C3-8F50-98FFE76C03C8}.Release|Any CPU.Build.0 = Release|Any CPU + {3B613E66-671E-4049-8EF5-43CDAA549210}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3B613E66-671E-4049-8EF5-43CDAA549210}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3B613E66-671E-4049-8EF5-43CDAA549210}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3B613E66-671E-4049-8EF5-43CDAA549210}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BAC8FB10-95ED-4FBB-9925-F6F0E15BD936} = {706260A8-86D4-432D-9FE0-F312863288F5} + {1F01E12A-50B7-4092-B30A-DEE9638FB753} = {706260A8-86D4-432D-9FE0-F312863288F5} + {53CDAFA0-30DF-404C-AAF0-652CA4047605} = {FE721A31-DF09-4E33-B791-BEC6C9E1C6F1} + {A7AE1C3F-CDC6-42DC-B4C1-5F7150661984} = {FE721A31-DF09-4E33-B791-BEC6C9E1C6F1} + {CA70C887-2A83-45DF-82EB-2DFFA8841B7B} = {706260A8-86D4-432D-9FE0-F312863288F5} + {CEF8B221-56E0-4777-88C1-9E3F9F3D1D3D} = {FE721A31-DF09-4E33-B791-BEC6C9E1C6F1} + {B6687100-3D8C-428C-8288-84607D9D5EDF} = {706260A8-86D4-432D-9FE0-F312863288F5} + {076940AD-70F3-47A8-827C-8E722714F937} = {706260A8-86D4-432D-9FE0-F312863288F5} + {8A23DF78-B3D6-41BD-BA50-19D0FBE4AB69} = {FE721A31-DF09-4E33-B791-BEC6C9E1C6F1} + {0C63EF8B-26F9-4511-9FC5-7431DE9657D6} = {3BC606D7-27B3-41FA-8FB3-9D56AC8B4DD7} + {ADE0E241-CBDD-48C3-8F50-98FFE76C03C8} = {706260A8-86D4-432D-9FE0-F312863288F5} + {3B613E66-671E-4049-8EF5-43CDAA549210} = {3BC606D7-27B3-41FA-8FB3-9D56AC8B4DD7} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {04241BED-1662-4690-BA56-15C99A840CFE} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\sources\editor\Stride.Core.MostRecentlyUsedFiles\Stride.Core.MostRecentlyUsedFiles.projitems*{78695de1-e621-45df-975d-cfd407081e23}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/sources/launcher/Stride.Launcher/App.axaml b/sources/launcher/Stride.Launcher/App.axaml new file mode 100644 index 0000000000..a31c2191fe --- /dev/null +++ b/sources/launcher/Stride.Launcher/App.axaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + 12 + 0,0,0,20 + + + + + + + + + + + + + + + + + diff --git a/sources/launcher/Stride.Launcher/App.axaml.cs b/sources/launcher/Stride.Launcher/App.axaml.cs new file mode 100644 index 0000000000..baca0e31f8 --- /dev/null +++ b/sources/launcher/Stride.Launcher/App.axaml.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Xaml; +using Stride.Core.Presentation.Avalonia.Services; +using Stride.Core.Presentation.ViewModels; +using Stride.Launcher.ViewModels; +using Stride.Launcher.Views; + +namespace Stride.Launcher; + +public partial class App : Application +{ + internal readonly CancellationTokenSource cts = new(); + + internal MainWindow? MainWindow { get; private set; } + + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + // Line below is needed to remove Avalonia data validation. + // Without this line you will get duplicate validations from both Avalonia and CT + BindingPlugins.DataValidators.RemoveAt(0); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = MainWindow = new() + { + DataContext = InitializeMainViewModel() + }; + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + // don't remove; also used by visual designer. + singleViewPlatform.MainView = new MainView + { + DataContext = InitializeMainViewModel() + }; + } + } + + private static MainViewModel InitializeMainViewModel() + { + return new(InitializeServiceProvider()); + } + + private static IViewModelServiceProvider InitializeServiceProvider() + { + var dispatcherService = DispatcherService.Create(); + var services = new object[] + { + dispatcherService, + new DialogService(dispatcherService) { ApplicationName = Launcher.ApplicationName } + }; + return new ViewModelServiceProvider(services); + } +} + +// This app is used for the crash report or for the notification when an instance is already running +internal sealed class MinimalApp : App +{ + public override void OnFrameworkInitializationCompleted() { } +} diff --git a/sources/launcher/Stride.Launcher/App.xaml b/sources/launcher/Stride.Launcher/App.xaml deleted file mode 100644 index dc853949d5..0000000000 --- a/sources/launcher/Stride.Launcher/App.xaml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/sources/launcher/Stride.Launcher/App.xaml.cs b/sources/launcher/Stride.Launcher/App.xaml.cs deleted file mode 100644 index 0ed6af7ada..0000000000 --- a/sources/launcher/Stride.Launcher/App.xaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) -// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -namespace Stride.LauncherApp -{ - /// - /// Interaction logic for App.xaml - /// - public partial class App - { - } -} diff --git a/sources/launcher/Stride.Launcher/Assets/CrashReportImage.png b/sources/launcher/Stride.Launcher/Assets/CrashReportImage.png new file mode 100644 index 0000000000..b1bf3de6f6 --- /dev/null +++ b/sources/launcher/Stride.Launcher/Assets/CrashReportImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36917dc9595f57e66f7d9692cd77fc19e8cc0f7eb31d76fe117d877ad72b2d31 +size 3019 diff --git a/sources/launcher/Stride.Launcher/Resources/EditorIcon.png b/sources/launcher/Stride.Launcher/Assets/Images/EditorIcon.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/EditorIcon.png rename to sources/launcher/Stride.Launcher/Assets/Images/EditorIcon.png diff --git a/sources/launcher/Stride.Launcher/Resources/chat-16.png b/sources/launcher/Stride.Launcher/Assets/Images/chat.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/chat-16.png rename to sources/launcher/Stride.Launcher/Assets/Images/chat.png diff --git a/sources/launcher/Stride.Launcher/Resources/delete-26-dark.png b/sources/launcher/Stride.Launcher/Assets/Images/delete.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/delete-26-dark.png rename to sources/launcher/Stride.Launcher/Assets/Images/delete.png diff --git a/sources/launcher/Stride.Launcher/Resources/discord.png b/sources/launcher/Stride.Launcher/Assets/Images/discord.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/discord.png rename to sources/launcher/Stride.Launcher/Assets/Images/discord.png diff --git a/sources/launcher/Stride.Launcher/Resources/download-26-dark.png b/sources/launcher/Stride.Launcher/Assets/Images/download.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/download-26-dark.png rename to sources/launcher/Stride.Launcher/Assets/Images/download.png diff --git a/sources/launcher/Stride.Launcher/Resources/facebook_24.png b/sources/launcher/Stride.Launcher/Assets/Images/facebook.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/facebook_24.png rename to sources/launcher/Stride.Launcher/Assets/Images/facebook.png diff --git a/sources/launcher/Stride.Launcher/Resources/getting-started.png b/sources/launcher/Stride.Launcher/Assets/Images/getting-started.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/getting-started.png rename to sources/launcher/Stride.Launcher/Assets/Images/getting-started.png diff --git a/sources/launcher/Stride.Launcher/Resources/github.png b/sources/launcher/Stride.Launcher/Assets/Images/github.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/github.png rename to sources/launcher/Stride.Launcher/Assets/Images/github.png diff --git a/sources/launcher/Stride.Launcher/Resources/issues.png b/sources/launcher/Stride.Launcher/Assets/Images/issues.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/issues.png rename to sources/launcher/Stride.Launcher/Assets/Images/issues.png diff --git a/sources/launcher/Stride.Launcher/Resources/list-26.png b/sources/launcher/Stride.Launcher/Assets/Images/list.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/list-26.png rename to sources/launcher/Stride.Launcher/Assets/Images/list.png diff --git a/sources/launcher/Stride.Launcher/Resources/news.png b/sources/launcher/Stride.Launcher/Assets/Images/news.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/news.png rename to sources/launcher/Stride.Launcher/Assets/Images/news.png diff --git a/sources/launcher/Stride.Launcher/Resources/note-26-dark.png b/sources/launcher/Stride.Launcher/Assets/Images/note.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/note-26-dark.png rename to sources/launcher/Stride.Launcher/Assets/Images/note.png diff --git a/sources/launcher/Stride.Launcher/Resources/opencollective_24.png b/sources/launcher/Stride.Launcher/Assets/Images/opencollective.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/opencollective_24.png rename to sources/launcher/Stride.Launcher/Assets/Images/opencollective.png diff --git a/sources/launcher/Stride.Launcher/Resources/recent-projects.png b/sources/launcher/Stride.Launcher/Assets/Images/recent-projects.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/recent-projects.png rename to sources/launcher/Stride.Launcher/Assets/Images/recent-projects.png diff --git a/sources/launcher/Stride.Launcher/Resources/reddit_24.png b/sources/launcher/Stride.Launcher/Assets/Images/reddit.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/reddit_24.png rename to sources/launcher/Stride.Launcher/Assets/Images/reddit.png diff --git a/sources/launcher/Stride.Launcher/Resources/roadmap.png b/sources/launcher/Stride.Launcher/Assets/Images/roadmap.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/roadmap.png rename to sources/launcher/Stride.Launcher/Assets/Images/roadmap.png diff --git a/sources/launcher/Stride.Launcher/Assets/Images/robot.png b/sources/launcher/Stride.Launcher/Assets/Images/robot.png new file mode 100644 index 0000000000..9e25c59043 --- /dev/null +++ b/sources/launcher/Stride.Launcher/Assets/Images/robot.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:876b4471b32cb4e9fc354e9d84519b06ca03cc6cdbc56f3780374e511512efdf +size 362967 diff --git a/sources/launcher/Stride.Launcher/Resources/showcase.png b/sources/launcher/Stride.Launcher/Assets/Images/showcase.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/showcase.png rename to sources/launcher/Stride.Launcher/Assets/Images/showcase.png diff --git a/sources/launcher/Stride.Launcher/Resources/survey.png b/sources/launcher/Stride.Launcher/Assets/Images/survey.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/survey.png rename to sources/launcher/Stride.Launcher/Assets/Images/survey.png diff --git a/sources/launcher/Stride.Launcher/Resources/switch-version.png b/sources/launcher/Stride.Launcher/Assets/Images/switch-version.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/switch-version.png rename to sources/launcher/Stride.Launcher/Assets/Images/switch-version.png diff --git a/sources/launcher/Stride.Launcher/Resources/twitch_24.png b/sources/launcher/Stride.Launcher/Assets/Images/twitch.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/twitch_24.png rename to sources/launcher/Stride.Launcher/Assets/Images/twitch.png diff --git a/sources/launcher/Stride.Launcher/Resources/update.png b/sources/launcher/Stride.Launcher/Assets/Images/update.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/update.png rename to sources/launcher/Stride.Launcher/Assets/Images/update.png diff --git a/sources/launcher/Stride.Launcher/Resources/upgrade-16.png b/sources/launcher/Stride.Launcher/Assets/Images/upgrade.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/upgrade-16.png rename to sources/launcher/Stride.Launcher/Assets/Images/upgrade.png diff --git a/sources/launcher/Stride.Launcher/Resources/visual-studio.png b/sources/launcher/Stride.Launcher/Assets/Images/visual-studio.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/visual-studio.png rename to sources/launcher/Stride.Launcher/Assets/Images/visual-studio.png diff --git a/sources/launcher/Stride.Launcher/Resources/xtwitter_24.png b/sources/launcher/Stride.Launcher/Assets/Images/xtwitter.png similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/xtwitter_24.png rename to sources/launcher/Stride.Launcher/Assets/Images/xtwitter.png diff --git a/sources/launcher/Stride.Launcher/Resources/Launcher.ico b/sources/launcher/Stride.Launcher/Assets/Launcher.ico similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/Launcher.ico rename to sources/launcher/Stride.Launcher/Assets/Launcher.ico diff --git a/sources/launcher/Stride.Launcher/Resources/Strings.Designer.cs b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs similarity index 99% rename from sources/launcher/Stride.Launcher/Resources/Strings.Designer.cs rename to sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs index 130b285152..4544ed1c8b 100644 --- a/sources/launcher/Stride.Launcher/Resources/Strings.Designer.cs +++ b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Stride.LauncherApp.Resources { +namespace Stride.Launcher.Assets.Localization { using System; @@ -39,7 +39,7 @@ internal Strings() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stride.LauncherApp.Resources.Strings", typeof(Strings).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stride.Launcher.Assets.Localization.Strings", typeof(Strings).Assembly); resourceMan = temp; } return resourceMan; @@ -139,9 +139,9 @@ public static string ButtonForums { /// /// Looks up a localized string similar to Fork on GitHub. /// - public static string ButtonGithub { + public static string ButtonGitHub { get { - return ResourceManager.GetString("ButtonGithub", resourceCulture); + return ResourceManager.GetString("ButtonGitHub", resourceCulture); } } diff --git a/sources/launcher/Stride.Launcher/Resources/Strings.ja-JP.resx b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.ja-JP.resx similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/Strings.ja-JP.resx rename to sources/launcher/Stride.Launcher/Assets/Localization/Strings.ja-JP.resx diff --git a/sources/launcher/Stride.Launcher/Resources/Strings.resx b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx similarity index 99% rename from sources/launcher/Stride.Launcher/Resources/Strings.resx rename to sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx index b75c9f282f..a783719fad 100644 --- a/sources/launcher/Stride.Launcher/Resources/Strings.resx +++ b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx @@ -133,7 +133,7 @@ Discuss about Stride /!\ Text must be short - + Fork on GitHub /!\ Text must be short diff --git a/sources/launcher/Stride.Launcher/Resources/Urls.Designer.cs b/sources/launcher/Stride.Launcher/Assets/Localization/Urls.Designer.cs similarity index 97% rename from sources/launcher/Stride.Launcher/Resources/Urls.Designer.cs rename to sources/launcher/Stride.Launcher/Assets/Localization/Urls.Designer.cs index d61a3b7c68..0be689bd72 100644 --- a/sources/launcher/Stride.Launcher/Resources/Urls.Designer.cs +++ b/sources/launcher/Stride.Launcher/Assets/Localization/Urls.Designer.cs @@ -8,7 +8,7 @@ // //------------------------------------------------------------------------------ -namespace Stride.LauncherApp.Resources { +namespace Stride.Launcher.Assets.Localization { using System; @@ -39,7 +39,7 @@ internal Urls() { public static global::System.Resources.ResourceManager ResourceManager { get { if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stride.LauncherApp.Resources.Urls", typeof(Urls).Assembly); + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Stride.Launcher.Assets.Localization.Urls", typeof(Urls).Assembly); resourceMan = temp; } return resourceMan; @@ -108,9 +108,9 @@ public static string GettingStarted { /// /// Looks up a localized string similar to https://github.com/stride3d/stride/. /// - public static string Github { + public static string GitHub { get { - return ResourceManager.GetString("Github", resourceCulture); + return ResourceManager.GetString("GitHub", resourceCulture); } } diff --git a/sources/launcher/Stride.Launcher/Resources/Urls.ja-JP.resx b/sources/launcher/Stride.Launcher/Assets/Localization/Urls.ja-JP.resx similarity index 100% rename from sources/launcher/Stride.Launcher/Resources/Urls.ja-JP.resx rename to sources/launcher/Stride.Launcher/Assets/Localization/Urls.ja-JP.resx diff --git a/sources/launcher/Stride.Launcher/Resources/Urls.resx b/sources/launcher/Stride.Launcher/Assets/Localization/Urls.resx similarity index 99% rename from sources/launcher/Stride.Launcher/Resources/Urls.resx rename to sources/launcher/Stride.Launcher/Assets/Localization/Urls.resx index 8ab986f887..ec6e7404d4 100644 --- a/sources/launcher/Stride.Launcher/Resources/Urls.resx +++ b/sources/launcher/Stride.Launcher/Assets/Localization/Urls.resx @@ -134,7 +134,7 @@ https://doc.stride3d.net/{0}/studio_getting_started_links.txt {0}: the major version of Stride (eg. 1.2) - + https://github.com/stride3d/stride/ @@ -161,4 +161,4 @@ https://visualstudio.microsoft.com/downloads - + \ No newline at end of file diff --git a/sources/launcher/Stride.Launcher/Constants.cs b/sources/launcher/Stride.Launcher/Constants.cs new file mode 100644 index 0000000000..b50f04c2ef --- /dev/null +++ b/sources/launcher/Stride.Launcher/Constants.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Launcher; + +internal struct Names +{ + public const string GameStudio = nameof(GameStudio); + public const string Stride = nameof(Stride); + public const string Xenko = nameof(Xenko); +} + +internal struct GameStudioNames +{ + public const string Stride = $"{Names.Stride}.{Names.GameStudio}"; + public const string StrideAvalonia = $"{Names.Stride}.{Names.GameStudio}.Avalonia.Desktop"; + public const string Xenko = $"{Names.Xenko}.{Names.GameStudio}"; +} diff --git a/sources/launcher/Stride.Launcher/Crash/CrashReportArgs.cs b/sources/launcher/Stride.Launcher/Crash/CrashReportArgs.cs new file mode 100644 index 0000000000..5933afe724 --- /dev/null +++ b/sources/launcher/Stride.Launcher/Crash/CrashReportArgs.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +namespace Stride.Crash; + +internal enum CrashLocation +{ + Main, + UnhandledException +} + +internal record CrashReportArgs +{ + public required Exception Exception { get; set; } + public required CrashLocation Location { get; set; } + public string[] Logs { get; set; } = []; + public string? ThreadName { get; set; } +} diff --git a/sources/launcher/Stride.Launcher/Crash/CrashReportData.cs b/sources/launcher/Stride.Launcher/Crash/CrashReportData.cs new file mode 100644 index 0000000000..6784925069 --- /dev/null +++ b/sources/launcher/Stride.Launcher/Crash/CrashReportData.cs @@ -0,0 +1,52 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Text; +using System.Text.Json; + +namespace Stride.Crash; + +public sealed class CrashReportData +{ + public List<(string key, object? value)> Data = []; + + public object? this[string key] + { + get => Data.Find(p => p.key == key).value; + set + { + if (value == null) + return; + + int num = -1; + foreach (var current in Data) + { + if (current.key == key) + { + num = Data.IndexOf(current); + break; + } + } + if (num != -1) + { + Data[num] = (key, value); + } + else + { + Data.Add((key, value)); + } + } + } + + public string ToJson() => JsonSerializer.Serialize(Data.ToDictionary()); + + public override string ToString() + { + StringBuilder val = new(); + foreach (var (key, value) in Data) + { + val.AppendLine($"{key}: {value}"); + } + return val.ToString(); + } +} diff --git a/sources/launcher/Stride.Launcher/Crash/CrashReportViewModel.cs b/sources/launcher/Stride.Launcher/Crash/CrashReportViewModel.cs new file mode 100644 index 0000000000..d8f891399f --- /dev/null +++ b/sources/launcher/Stride.Launcher/Crash/CrashReportViewModel.cs @@ -0,0 +1,118 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Diagnostics; +using System.Text; +using Stride.Core.Extensions; +using Stride.Core.Presentation.Avalonia.Services; +using Stride.Core.Presentation.Commands; +using Stride.Core.Presentation.Services; +using Stride.Core.Presentation.ViewModels; + +namespace Stride.Crash.ViewModels; + +internal sealed class CrashReportViewModel : ViewModelBase +{ + private readonly string applicationName; + private readonly CancellationTokenSource exitToken; + private readonly Func setClipboard; + + private bool isReportVisible; + + public CrashReportViewModel(string applicationName, CrashReportArgs args, Func setClipboard, CancellationTokenSource exitToken) + : base(new ViewModelServiceProvider()) + { + this.applicationName = applicationName; + this.exitToken = exitToken; + this.setClipboard = setClipboard; + + var dispatcher = DispatcherService.Create(); + ServiceProvider.RegisterService(dispatcher); + ServiceProvider.RegisterService(new DialogService(dispatcher) { ApplicationName = applicationName }); + + Report = ComputeReport(args); + + CopyReportCommand = new AnonymousTaskCommand(ServiceProvider, OnCopyReport); + CloseCommand = new AnonymousCommand(ServiceProvider, OnClose); + OpenIssueCommand = new AnonymousTaskCommand(ServiceProvider, OnOpenIssue); + ViewReportCommand = new AnonymousCommand(ServiceProvider, OnViewReport); + } + + public string ApplicationName => applicationName; + + public bool IsReportVisible + { + get => isReportVisible; + set => SetValue(ref isReportVisible, value); + } + + public CrashReportData Report { get; } + + public ICommandBase CopyReportCommand { get; } + public ICommandBase CloseCommand { get; } + public ICommandBase OpenIssueCommand { get; } + public ICommandBase ViewReportCommand { get; } + + private void OnClose() + { + exitToken.Cancel(); + } + + private Task OnCopyReport() + { + return setClipboard.Invoke(Report.ToJson()); + } + + private async Task OnOpenIssue() + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = "https://github.com/stride3d/stride/issues/new?labels=bug&template=bug_report.md&", + UseShellExecute = true + }); + } + // FIXME: catch only specific exceptions? + catch (Exception) + { + DialogService.MainWindow!.Topmost = false; + // FIXME: localize resource string + await ServiceProvider.Get().MessageBoxAsync("An error occurred while trying to open a web browser", MessageBoxButton.OK, MessageBoxImage.Error); + } + } + + private void OnViewReport() + { + IsReportVisible = true; + } + + private CrashReportData ComputeReport(CrashReportArgs args) + { + return new() + { + ["Application"] = applicationName, + ["ThreadName"] = args.ThreadName, +#if DEBUG + ["ProcessID"] = Environment.ProcessId, + ["CurrentDirectory"] = Environment.CurrentDirectory, +#endif + ["OsArch"] = Environment.Is64BitOperatingSystem ? "x64" : "x86", + ["OsVersion"] = Environment.OSVersion, + ["ProcessorCount"] = Environment.ProcessorCount, + ["Exception"] = args.Exception.FormatFull(), + ["LastLogs"] = FormatLogs(args.Logs), + }; + + static string FormatLogs(string[] logs) + { + var builder = new StringBuilder(); + for (var i = 0; i < logs.Length; i++) + { + var log = logs[i]; + builder.AppendLine($"{i + 1}: {log}"); + } + return builder.ToString(); + } + } +} diff --git a/sources/launcher/Stride.Launcher/Crash/CrashReportWindow.axaml b/sources/launcher/Stride.Launcher/Crash/CrashReportWindow.axaml new file mode 100644 index 0000000000..d2119585a6 --- /dev/null +++ b/sources/launcher/Stride.Launcher/Crash/CrashReportWindow.axaml @@ -0,0 +1,47 @@ + + + + + + + + + + Unfortunately, has crashed. + Please help us improve Stride by sending information about this crash through Github Issues. + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - From 7be44f2d5ac20a479e6979a8b33c46bd79418b4e Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sat, 2 May 2026 23:29:28 +0200 Subject: [PATCH 39/46] docs(launcher): update port-status.md corrections - Mark 'Show in Explorer' as fixed (cross-platform since commit c2555d0d): update 'already ported' bullet and strike through the regression entry with the three-platform implementation detail - Correct slide animation duration from 0.5s to 1s in both the Fixed description and the Phase 3 roadmap entries (1s was intentional) --- docs/launcher/port-status.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/launcher/port-status.md b/docs/launcher/port-status.md index d2d80e224c..3e0cb5ddf6 100644 --- a/docs/launcher/port-status.md +++ b/docs/launcher/port-status.md @@ -17,7 +17,7 @@ The core is in place: - Recent projects + MRU integration with Game Studio. - VSIX discovery via `VisualStudioVersions` (no-op on Linux, by design). - `ShowBetaVersions` toggle with Avalonia `Interaction.Behaviors` / `DataTriggerBehavior` (ported correctly). -- Recent-project context menu with *Show in Explorer* / *Remove from list* (menu ported; *Show in Explorer* implementation is still Windows-only, see below). +- Recent-project context menu with *Show in Explorer* / *Remove from list* (menu ported; *Show in Explorer* is cross-platform — Windows `explorer.exe /select`, macOS `open -R`, Linux DBus `FileManager1.ShowItems` with `xdg-open` fallback). - Alternate-versions sub-list (ported as a nested `ItemsControl`, no longer a `Popup`). - Localization resx / Urls resx. @@ -71,7 +71,7 @@ Fixed: [App.axaml.cs](../../sources/launcher/Stride.Launcher/App.axaml.cs) `OnLi ~~Current [Announcement.axaml](../../sources/launcher/Stride.Launcher/Views/Announcement.axaml) is a plain `DockPanel` with no transform and no animation — the overlay pops in and out discretely.~~ -Fixed: the announcement overlay `Border` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml) now uses a `TransformOperationsTransition` (`translateX(0)` ↔ `translateX(100%)`, 0.5s, `CubicEaseOut`) driven by `Classes.visible`. +Fixed: the announcement overlay `Border` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml) now uses a `TransformOperationsTransition` (`translateX(0)` ↔ `translateX(100%)`, 1s, `CubicEaseOut`) driven by `Classes.visible`. ### ~~Release-notes panel lost its slide animation~~ — Fixed (Phase 3) @@ -79,15 +79,17 @@ Fixed: the announcement overlay `Border` in [MainView.axaml](../../sources/launc Fixed: the release-notes `Grid` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml) now uses the same `TransformOperationsTransition` approach, driven by `ActiveReleaseNotes.IsActive`. -### "Show in Explorer" is hard-wired to `explorer.exe` +### ~~"Show in Explorer" is hard-wired to `explorer.exe`~~ — Fixed (Phase 1) -[RecentProjectViewModel.cs:72](../../sources/launcher/Stride.Launcher/ViewModels/RecentProjectViewModel.cs#L72): +~~[RecentProjectViewModel.cs:72](../../sources/launcher/Stride.Launcher/ViewModels/RecentProjectViewModel.cs#L72):~~ ```csharp var startInfo = new ProcessStartInfo("explorer.exe", $"/select,{fullPath.ToOSPath()}") { UseShellExecute = true }; ``` -The menu item is visible on Linux but invocation will fail. Needs a platform switch (`xdg-open {dir}` on Linux, `open -R {path}` on macOS). +~~The menu item is visible on Linux but invocation will fail. Needs a platform switch (`xdg-open {dir}` on Linux, `open -R {path}` on macOS).~~ + +Fixed: `RecentProjectViewModel.Explore` has a full platform switch — Windows `explorer.exe /select`, macOS `open -R`, Linux DBus `org.freedesktop.FileManager1.ShowItems` with `xdg-open {parent-dir}` fallback. See [cross-platform.md](cross-platform.md) § Recent-project "Show in Explorer". ### `SiliconStudioStrideDir` / `StrideDir` env vars no longer seeded @@ -160,8 +162,8 @@ These change observable behaviour on both Windows and Linux and should ship firs Nice-to-have UX polish. The entries below all map to Avalonia `Transitions` on the relevant `TranslateTransform.X` / `Opacity`, which is how animations are expressed in Avalonia (versus WPF's `Storyboard`/`DoubleAnimation`). -1. ~~**Announcement slide-in/out**~~ **Done**: `TransformOperationsTransition` on `RenderTransform` (`translateX(0)` ↔ `translateX(100%)`), 0.5s, `CubicEaseOut`. Implemented via CSS-class binding (`Classes.visible`) on the overlay `Border` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml). Root `Grid` gained `ClipToBounds="True"` to prevent the panel from showing while off-screen. -2. ~~**Release-notes panel slide**~~ **Done**: Same approach — `TransformOperationsTransition` on the release-notes `Grid` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml), bound to `ActiveReleaseNotes.IsActive`. The main-content grid's `IsVisible` binding was replaced with `IsEnabled`-only (layout stays; the release-notes panel renders on top while sliding in). +1. ~~**Announcement slide-in/out**~~ **Done**: `TransformOperationsTransition` on `RenderTransform` (`translateX(0)` ↔ `translateX(100%)`), 1s, `CubicEaseOut`. Implemented via CSS-class binding (`Classes.visible`) on the overlay `Border` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml). Root `Grid` gained `ClipToBounds="True"` to prevent the panel from showing while off-screen. +2. ~~**Release-notes panel slide**~~ **Done**: Same approach — `TransformOperationsTransition` on the release-notes `Grid` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml), bound to `ActiveReleaseNotes.IsActive`, 1s, `CubicEaseOut`. The main-content grid's `IsVisible` binding was replaced with `IsEnabled`-only (layout stays; the release-notes panel renders on top while sliding in). 3. ~~**`CurrentToolTip` status-line behavior**~~ **Already done** (pre-existing): `BindCurrentToolTipStringBehavior` is implemented in `Stride.Core.Presentation.Avalonia` and wired on every relevant control in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml). `MainViewModel.CurrentToolTip` property also present. 4. ~~**Optional: revisit alternate-versions UX.**~~ **Already done** (pre-existing): The current branch uses a `ToggleButton` + `Popup` with `IsLightDismissEnabled="True"` and an `EventTriggerBehavior` that closes the popup on item click — functionally equivalent to the master `StaysOpenContextMenu` approach. From cb7f8e1a1c363f583382246ce08dfb8ccd3bbc12 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sun, 10 May 2026 13:19:16 +0200 Subject: [PATCH 40/46] fix(launcher): alignment and text trimming in recent projects --- sources/launcher/Stride.Launcher/Views/MainView.axaml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sources/launcher/Stride.Launcher/Views/MainView.axaml b/sources/launcher/Stride.Launcher/Views/MainView.axaml index 0f46c19c42..a4867c9e90 100644 --- a/sources/launcher/Stride.Launcher/Views/MainView.axaml +++ b/sources/launcher/Stride.Launcher/Views/MainView.axaml @@ -575,15 +575,18 @@ - - Date: Fri, 15 May 2026 10:00:39 +0200 Subject: [PATCH 41/46] feat(launcher): cross-platform GameStudio IPC - Add cross-platform IPC: Windows HWND broadcast + Linux named pipe - Update launcher documentation (port-status.md, cross-platform.md) --- docs/launcher/cross-platform.md | 11 ++++- docs/launcher/port-status.md | 2 + .../ViewModels/MainViewModel.cs | 47 +++++++++++++++++-- .../Stride.Launcher/Views/MainWindow.axaml.cs | 20 ++++++++ 4 files changed, 74 insertions(+), 6 deletions(-) diff --git a/docs/launcher/cross-platform.md b/docs/launcher/cross-platform.md index db75513032..7c0284b2bc 100644 --- a/docs/launcher/cross-platform.md +++ b/docs/launcher/cross-platform.md @@ -33,9 +33,16 @@ Both `Stride.Metrics` / `MetricsClient` (telemetry) and `PrivacyPolicyHelper` (f ## Launcher ↔ GameStudio IPC -When `AutoCloseLauncher` is on, the launcher passes its own Win32 window handle to Game Studio via the `/LauncherWindowHandle ` argument. Game Studio uses it later to `PostMessage` back at the launcher and ask it to close itself. +When `AutoCloseLauncher` is on, the launcher passes IPC information to Game Studio via a CLI argument so Game Studio can signal back once it has started. -The handle is captured in `MainWindow.OnOpened` only when `OperatingSystem.IsWindows()` is true; on Linux it stays `IntPtr.Zero` and Game Studio's parser ignores it. This is fine on the current branch because Game Studio itself is still Windows-only — a separate effort (`xplat-editor`) is porting it to Avalonia. When that lands, the HWND-based channel will need to be replaced with a cross-platform IPC token (named pipe, socket, or similar), passed via a generalised CLI argument. See [port-status.md](port-status.md) Phase 1 for the rationale. +| Platform | Argument | Mechanism | +|---|---|---| +| Windows | `/LauncherWindowHandle ` | Game Studio sends a Win32 `WM_CLOSE` message to the launcher's HWND. `MainWindow.OnOpened` captures the HWND via `TryGetPlatformHandle()`. | +| Linux (and other non-Windows) | `/LauncherPipe ` | The launcher starts a `NamedPipeServerStream` and waits for a connection (up to 2 minutes). Game Studio connects via `NamedPipeClientStream` once its main window is loaded and writes one byte. The launcher raises `CloseRequested` and the window closes gracefully. | + +Both paths are implemented: +- **Launcher side:** `MainViewModel.StartStudio` — branches on `OperatingSystem.IsWindows()`. Non-Windows path starts `WaitForGameStudioPipeSignalAsync` and injects `/LauncherPipe `. `MainWindow.axaml.cs` wires `CloseRequested → OnClosingAsync`. +- **Game Studio side:** `Program.ParseLauncherArgs` (in `Stride.GameStudio.Avalonia.Desktop`) parses both argument styles and sets `App.LauncherNotifier`. `MainWindow.OnLoaded` fires it once. ## Settings paths diff --git a/docs/launcher/port-status.md b/docs/launcher/port-status.md index 3e0cb5ddf6..a48130df8e 100644 --- a/docs/launcher/port-status.md +++ b/docs/launcher/port-status.md @@ -20,6 +20,7 @@ The core is in place: - Recent-project context menu with *Show in Explorer* / *Remove from list* (menu ported; *Show in Explorer* is cross-platform — Windows `explorer.exe /select`, macOS `open -R`, Linux DBus `FileManager1.ShowItems` with `xdg-open` fallback). - Alternate-versions sub-list (ported as a nested `ItemsControl`, no longer a `Popup`). - Localization resx / Urls resx. +- **Cross-platform launcher ↔ Game Studio IPC**: on Windows the existing `/LauncherWindowHandle` Win32 HWND path is unchanged; on Linux a named-pipe channel (`/LauncherPipe `) is used instead. See [cross-platform.md](cross-platform.md) § Launcher ↔ GameStudio IPC. ## Flagged gaps (already in [cross-platform.md](cross-platform.md)) @@ -150,6 +151,7 @@ These change observable behaviour on both Windows and Linux and should ship firs 3. ~~**Persist `LauncherSettings.CurrentTab` on tab change.**~~ **Done** (2026-04-22): `MainViewModel.CurrentTab` persisted-on-set, two-way bound from the `TabControl`. 4. ~~**Port `HasDoneTask` / `SaveTaskAsDone` to a file under `EditorPath.UserDataPath`.**~~ **Done** (2026-04-22): one-shot task state moved into `Internal/Launcher/CompletedTasks` inside the existing `LauncherSettings.conf`. No migration — pre-existing `HKCU\SOFTWARE\Stride\` keys on Windows become harmless orphans. 5. ~~**Fix `explorer.exe` hard-coding** in `RecentProjectViewModel.Explore`.~~ **Done** (2026-04-22): platform switch — Windows `explorer.exe /select`, macOS `open -R`, Linux DBus `FileManager1.ShowItems` with `xdg-open` fallback. See [cross-platform.md](cross-platform.md) § Recent-project "Show in Explorer". +6. ~~**Cross-platform launcher ↔ Game Studio IPC.**~~ **Done** (2026-05-09): on Linux the HWND channel is replaced by a named pipe. `MainViewModel.StartStudio` injects `/LauncherPipe ` on non-Windows and starts `WaitForGameStudioPipeSignalAsync`; `Program.ParseLauncherArgs` in `Stride.GameStudio.Avalonia.Desktop` handles both `/LauncherWindowHandle` (Windows) and `/LauncherPipe` (Linux). `App.LauncherNotifier` fires once from `MainWindow.OnLoaded`. See [cross-platform.md](cross-platform.md) § Launcher ↔ GameStudio IPC. ### Phase 2 — feature restoration diff --git a/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs b/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs index 4a6dc3ee4f..2bfd59c1ca 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.IO.Pipes; using Stride.Core.CodeEditorSupport.VisualStudio; using Stride.Core.Extensions; using Stride.Core.Packages; @@ -115,6 +116,12 @@ public void Dispose() public static IntPtr WindowHandle { get; set; } + /// + /// Raised when the launcher should close itself — e.g. because Game Studio signalled back + /// via the cross-platform pipe IPC (Linux) or a Win32 WM_CLOSE (Windows). + /// + internal event EventHandler? CloseRequested; + public IEnumerable StrideVersions => strideVersions; public bool ShowBetaVersions { get { return showBetaVersions; } set { SetValue(ref showBetaVersions, value); } } @@ -638,10 +645,20 @@ public async Task StartStudio(string argument) if (AutoCloseLauncher) { - // WindowHandle is a Win32 HWND populated by MainWindow.OnOpened on Windows only. - // On Linux it stays IntPtr.Zero — Game Studio's parser tolerates 0. See - // MainWindow.OnOpened for what needs to change when xplat-GameStudio lands. - argument = $"/LauncherWindowHandle {WindowHandle} {argument}"; + if (OperatingSystem.IsWindows()) + { + // WindowHandle is a Win32 HWND populated by MainWindow.OnOpened on Windows only. + argument = $"/LauncherWindowHandle {WindowHandle} {argument}"; + } + else + { + // On Linux (and other non-Windows platforms) the Avalonia GameStudio uses a named + // pipe to signal the launcher that it has started. The launcher listens on the pipe + // and raises CloseRequested when a connection arrives. + var pipeName = $"stride-launcher-{Environment.ProcessId}"; + _ = WaitForGameStudioPipeSignalAsync(pipeName); + argument = $"/LauncherPipe {pipeName} {argument}"; + } } try @@ -684,6 +701,28 @@ await Dispatcher.InvokeAsync(() => }); } + /// + /// Waits for Game Studio to connect to the named pipe and signal that it has started, + /// then raises so that the launcher window closes gracefully. + /// Used on Linux (and other non-Windows platforms) as an alternative to the Win32 + /// /LauncherWindowHandle IPC mechanism. + /// + private async Task WaitForGameStudioPipeSignalAsync(string pipeName) + { + try + { + using var server = new NamedPipeServerStream(pipeName, PipeDirection.In, 1, + PipeTransmissionMode.Byte, PipeOptions.Asynchronous); + // Wait up to 2 minutes for Game Studio to start and connect + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); + await server.WaitForConnectionAsync(cts.Token); + // Game Studio connected — ask the window to close gracefully + await Dispatcher.InvokeAsync(() => CloseRequested?.Invoke(this, EventArgs.Empty)); + } + catch (OperationCanceledException) { } + catch (Exception) { } + } + private async Task InstallLatestVersion() { var latestVersion = strideVersions.FirstOrDefault(); diff --git a/sources/launcher/Stride.Launcher/Views/MainWindow.axaml.cs b/sources/launcher/Stride.Launcher/Views/MainWindow.axaml.cs index 28cac49aa3..d383b95d80 100644 --- a/sources/launcher/Stride.Launcher/Views/MainWindow.axaml.cs +++ b/sources/launcher/Stride.Launcher/Views/MainWindow.axaml.cs @@ -8,9 +8,29 @@ namespace Stride.Launcher.Views; public partial class MainWindow : Window { + private MainViewModel? _subscribedVm; + public MainWindow() { InitializeComponent(); + DataContextChanged += OnDataContextChanged; + } + + private void OnDataContextChanged(object? sender, EventArgs e) + { + if (_subscribedVm is not null) + _subscribedVm.CloseRequested -= OnCloseRequested; + + _subscribedVm = DataContext as MainViewModel; + + if (_subscribedVm is not null) + _subscribedVm.CloseRequested += OnCloseRequested; + } + + private void OnCloseRequested(object? sender, EventArgs e) + { + if (DataContext is MainViewModel vm) + _ = OnClosingAsync(vm); } protected override void OnOpened(EventArgs e) From 6d262d66a51156d0c3d56da2f5b785f280280db8 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sun, 10 May 2026 12:10:22 +0200 Subject: [PATCH 42/46] feat(launcher): preferred-editor selector, companion package install - Add editor selector ComboBox to choose between WPF and Avalonia GameStudio - Auto-install companion editor package (WPF/Avalonia) after primary install - Query NuGet store directly for installed editor paths (works across sessions) - Persist preferred editor choice in launcher settings - Add EditorNameConverter for friendly display names - Disable editor/framework selectors during install - Update launcher documentation (port-status.md) --- docs/launcher/port-status.md | 1 + .../Helpers/InMemoryLauncherSettings.cs | 1 + .../Services/ILauncherSettingsService.cs | 1 + .../Services/LauncherSettings.cs | 5 + .../Services/LauncherSettingsService.cs | 6 + .../ViewModels/EditorNameConverter.cs | 25 +++ .../ViewModels/MainViewModel.cs | 13 ++ .../ViewModels/PackageVersionViewModel.cs | 11 ++ .../ViewModels/StrideStoreVersionViewModel.cs | 64 ++++++++ .../ViewModels/StrideVersionViewModel.cs | 147 ++++++++++++++++-- .../Stride.Launcher/Views/MainView.axaml | 16 +- .../Stride.Launcher/Views/MainView.axaml.cs | 10 ++ 12 files changed, 288 insertions(+), 12 deletions(-) create mode 100644 sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs diff --git a/docs/launcher/port-status.md b/docs/launcher/port-status.md index a48130df8e..4758662fd2 100644 --- a/docs/launcher/port-status.md +++ b/docs/launcher/port-status.md @@ -20,6 +20,7 @@ The core is in place: - Recent-project context menu with *Show in Explorer* / *Remove from list* (menu ported; *Show in Explorer* is cross-platform — Windows `explorer.exe /select`, macOS `open -R`, Linux DBus `FileManager1.ShowItems` with `xdg-open` fallback). - Alternate-versions sub-list (ported as a nested `ItemsControl`, no longer a `Popup`). - Localization resx / Urls resx. +- **Preferred-editor selector**: when both `Stride.GameStudio.Avalonia.Desktop` (Avalonia) and `Stride.GameStudio` (WPF) packages are installed for the same major.minor version, a `ComboBox` appears in the launcher to choose between them. The selection is persisted as `PreferredEditor` in `LauncherSettings.conf`. `StrideVersionViewModel.UpdateAvailableEditors` scans all installed package paths (primary + alternates via `GetAllInstalledPaths`) to find which executables are available; `LocateMainExecutable` routes to the correct per-editor install directory using the `_editorToInstallPath` map. - **Cross-platform launcher ↔ Game Studio IPC**: on Windows the existing `/LauncherWindowHandle` Win32 HWND path is unchanged; on Linux a named-pipe channel (`/LauncherPipe `) is used instead. See [cross-platform.md](cross-platform.md) § Launcher ↔ GameStudio IPC. ## Flagged gaps (already in [cross-platform.md](cross-platform.md)) diff --git a/sources/launcher/Stride.Launcher.Tests/Helpers/InMemoryLauncherSettings.cs b/sources/launcher/Stride.Launcher.Tests/Helpers/InMemoryLauncherSettings.cs index b204e31a01..ac7cc5a7d4 100644 --- a/sources/launcher/Stride.Launcher.Tests/Helpers/InMemoryLauncherSettings.cs +++ b/sources/launcher/Stride.Launcher.Tests/Helpers/InMemoryLauncherSettings.cs @@ -10,6 +10,7 @@ internal sealed class InMemoryLauncherSettings : ILauncherSettingsService public bool CloseLauncherAutomatically { get; set; } public string ActiveVersion { get; set; } = ""; public string PreferredFramework { get; set; } = "net10.0"; + public string PreferredEditor { get; set; } = ""; public int CurrentTab { get; set; } public IReadOnlyCollection DeveloperVersions { get; init; } = []; diff --git a/sources/launcher/Stride.Launcher/Services/ILauncherSettingsService.cs b/sources/launcher/Stride.Launcher/Services/ILauncherSettingsService.cs index af25dfef2a..cba77f46b5 100644 --- a/sources/launcher/Stride.Launcher/Services/ILauncherSettingsService.cs +++ b/sources/launcher/Stride.Launcher/Services/ILauncherSettingsService.cs @@ -10,6 +10,7 @@ public interface ILauncherSettingsService bool CloseLauncherAutomatically { get; set; } string ActiveVersion { get; set; } string PreferredFramework { get; set; } + string PreferredEditor { get; set; } int CurrentTab { get; set; } IReadOnlyCollection DeveloperVersions { get; } bool IsTaskCompleted(string taskName); diff --git a/sources/launcher/Stride.Launcher/Services/LauncherSettings.cs b/sources/launcher/Stride.Launcher/Services/LauncherSettings.cs index b120688ab9..814a60df84 100644 --- a/sources/launcher/Stride.Launcher/Services/LauncherSettings.cs +++ b/sources/launcher/Stride.Launcher/Services/LauncherSettings.cs @@ -14,6 +14,7 @@ public static class LauncherSettings private static readonly SettingsKey CloseLauncherAutomaticallyKey = new("Internal/Launcher/CloseLauncherAutomatically", SettingsContainer, false); private static readonly SettingsKey ActiveVersionKey = new("Internal/Launcher/ActiveVersion", SettingsContainer, ""); private static readonly SettingsKey PreferredFrameworkKey = new("Internal/Launcher/PreferredFramework", SettingsContainer, "net10.0"); + private static readonly SettingsKey PreferredEditorKey = new("Internal/Launcher/PreferredEditor", SettingsContainer, ""); private static readonly SettingsKey CurrentTabKey = new("Internal/Launcher/CurrentTabSessions", SettingsContainer, 0); private static readonly SettingsKey> DeveloperVersionsKey = new("Internal/Launcher/DeveloperVersions", SettingsContainer, () => new List()); private static readonly SettingsKey> CompletedTasksKey = new("Internal/Launcher/CompletedTasks", SettingsContainer, () => new List()); @@ -28,6 +29,7 @@ static LauncherSettings() CloseLauncherAutomatically = CloseLauncherAutomaticallyKey.GetValue(); ActiveVersion = ActiveVersionKey.GetValue(); PreferredFramework = PreferredFrameworkKey.GetValue(); + PreferredEditor = PreferredEditorKey.GetValue(); CurrentTab = CurrentTabKey.GetValue(); DeveloperVersions = DeveloperVersionsKey.GetValue(); completedTasks = CompletedTasksKey.GetValue(); @@ -38,6 +40,7 @@ public static void Save() CloseLauncherAutomaticallyKey.SetValue(CloseLauncherAutomatically); ActiveVersionKey.SetValue(ActiveVersion); PreferredFrameworkKey.SetValue(PreferredFramework); + PreferredEditorKey.SetValue(PreferredEditor); CurrentTabKey.SetValue(CurrentTab); CompletedTasksKey.SetValue(completedTasks); SettingsContainer.SaveSettingsProfile(SettingsContainer.CurrentProfile, LauncherConfigPath); @@ -51,6 +54,8 @@ public static void Save() public static string PreferredFramework { get; set; } + public static string PreferredEditor { get; set; } + public static int CurrentTab { get; set; } public static IReadOnlyCollection CompletedTasks => completedTasks; diff --git a/sources/launcher/Stride.Launcher/Services/LauncherSettingsService.cs b/sources/launcher/Stride.Launcher/Services/LauncherSettingsService.cs index b833429cf6..dfed1cf5fc 100644 --- a/sources/launcher/Stride.Launcher/Services/LauncherSettingsService.cs +++ b/sources/launcher/Stride.Launcher/Services/LauncherSettingsService.cs @@ -25,6 +25,12 @@ public string PreferredFramework set => LauncherSettings.PreferredFramework = value; } + public string PreferredEditor + { + get => LauncherSettings.PreferredEditor; + set => LauncherSettings.PreferredEditor = value; + } + public int CurrentTab { get => LauncherSettings.CurrentTab; diff --git a/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs b/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs new file mode 100644 index 0000000000..45e86db4c7 --- /dev/null +++ b/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) +// Distributed under the MIT license. See the LICENSE.md file in the project root for more information. + +using System.Globalization; +using Stride.Core.Presentation.Avalonia.Converters; + +namespace Stride.Launcher.ViewModels; + +/// +/// Converts an internal editor executable name (e.g. Stride.GameStudio.Avalonia.Desktop) +/// to a user-friendly display name. +/// +public sealed class EditorNameConverter : OneWayValueConverter +{ + public override object Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + return (string?)value switch + { + GameStudioNames.StrideAvalonia => "Game Studio (Avalonia)", + GameStudioNames.Stride => "Game Studio (WPF)", + GameStudioNames.Xenko => "Xenko Game Studio", + var name => name ?? string.Empty, + }; + } +} diff --git a/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs b/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs index 2bfd59c1ca..1f0fad1efc 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs @@ -206,6 +206,19 @@ public string PreferredFramework } } + public string PreferredEditor + { + get => _settings.PreferredEditor; + set + { + if (_settings.PreferredEditor != value) + { + _settings.PreferredEditor = value; + _settings.Save(); + } + } + } + public int CurrentTab { get => currentTab; diff --git a/sources/launcher/Stride.Launcher/ViewModels/PackageVersionViewModel.cs b/sources/launcher/Stride.Launcher/ViewModels/PackageVersionViewModel.cs index f7bc1f9353..22bfbdfead 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/PackageVersionViewModel.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/PackageVersionViewModel.cs @@ -2,6 +2,7 @@ // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. using System.Diagnostics; +using Stride.Core; using Stride.Core.Extensions; using Stride.Core.Packages; using Stride.Core.Presentation.Commands; @@ -160,6 +161,15 @@ protected virtual void AfterDownload() // Intentionally does nothing. } + /// + /// Attempts to install companion packages alongside the primary one (e.g. the Avalonia + /// editor alongside the WPF one, or vice versa). Failures are silently ignored so that + /// missing companions — which is the normal state once only one editor flavour is + /// published — do not block the install. + /// + /// The version that was just installed. + protected virtual Task TryInstallCompanionsAsync(PackageVersion version) => Task.CompletedTask; + /// /// Downloads the latest version of this package. If a version is already in the local store, it will be deleted first. /// @@ -219,6 +229,7 @@ public Task Download(bool displayErrorMessage) downloadCompleted = true; } + await TryInstallCompanionsAsync(ServerPackage.Version); AfterDownload(); } catch (Exception e) diff --git a/sources/launcher/Stride.Launcher/ViewModels/StrideStoreVersionViewModel.cs b/sources/launcher/Stride.Launcher/ViewModels/StrideStoreVersionViewModel.cs index 0e7bdbc5e5..b721665eac 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/StrideStoreVersionViewModel.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/StrideStoreVersionViewModel.cs @@ -80,6 +80,20 @@ public override string FullName public ObservableList AlternateVersions { get; } = []; + // All local install paths for this major.minor slot across every package ID (WPF, Avalonia, …). + // Populated by UpdateLocalPackage from the full NuGet package group; used by GetAllInstalledPaths. + private List _allLocalPackagePaths = []; + + /// + /// + /// Returns the install paths of every locally-installed package that belongs to this + /// major.minor version slot (e.g. both Stride.GameStudio and + /// Stride.GameStudio.Avalonia.Desktop at 4.3.1). The paths are captured + /// from the raw NuGet package group in before + /// collapses same-version entries. + /// + protected override IEnumerable GetAllInstalledPaths() => _allLocalPackagePaths; + /// /// Gets the release notes associated to this version. /// @@ -106,6 +120,26 @@ internal void UpdateLocalPackage(NugetLocalPackage? package, IEnumerable(); + foreach (var id in Store.MainPackageIds) + { + var installedPath = Store.GetInstalledPath(id, package.Version); + if (Directory.Exists(installedPath) && !paths.Contains(installedPath)) + paths.Add(installedPath); + } + _allLocalPackagePaths = paths; + } + else + { + _allLocalPackagePaths = []; + } + if (alternateVersions is not null) { Dispatcher.Invoke(() => @@ -258,6 +292,36 @@ protected override void UpdateInstallStatus() } } + /// + /// + /// Silently installs the companion Stride editor package at the same version (e.g. + /// Stride.GameStudio.Avalonia.Desktop when the primary is Stride.GameStudio, + /// or vice versa). If the companion package does not exist on the feed — which is the + /// expected state once only one editor flavour is published — the failure is swallowed + /// and the install continues normally. + /// + protected override async Task TryInstallCompanionsAsync(PackageVersion version) + { + var companionIds = new[] { GameStudioNames.StrideAvalonia, GameStudioNames.Stride } + .Where(id => id != ServerPackage?.Id); + + foreach (var companionId in companionIds) + { + // Skip if already installed at this version. + if (Store.FindLocalPackage(companionId, version) is not null) + continue; + + try + { + await Store.InstallPackage(companionId, version, [], null); + } + catch + { + // Package not available on the feed for this version — skip silently. + } + } + } + /// protected override void BeforeDownload() { diff --git a/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs b/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs index 659a9b4703..c3afef9b73 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs @@ -16,6 +16,11 @@ public abstract class StrideVersionViewModel : PackageVersionViewModel, ICompara private bool isVisible; private bool canStart; private string? selectedFramework; + private string? selectedEditor; + // Maps each discovered editor name to its fully-resolved directory (including TFM subfolder). + // e.g. "Stride.GameStudio.Avalonia.Desktop" → ".../lib/net10.0" + // Populated by UpdateAvailableEditors; consumed by LocateMainExecutable. + private readonly Dictionary _editorToDir = []; internal StrideVersionViewModel(MainViewModel launcher, NugetStore store, NugetLocalPackage? localPackage, string packageId, int major, int minor) : base(launcher, store, localPackage) @@ -30,6 +35,18 @@ internal StrideVersionViewModel(MainViewModel launcher, NugetStore store, NugetL launcher.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(MainViewModel.ShowBetaVersions)) UpdateStatus(); }; } + /// + /// Returns all local install base paths that belong to this version slot. + /// The base implementation returns only the primary . + /// Subclasses such as override this to also + /// include alternate package paths (e.g. a sibling Avalonia or WPF package). + /// + protected virtual IEnumerable GetAllInstalledPaths() + { + if (InstallPath is not null) + yield return InstallPath; + } + protected static string[] GetExecutableNames() { return OperatingSystem.IsWindows() @@ -93,6 +110,70 @@ internal void UpdateSelectedFramework() SelectedFramework = Frameworks.First(); } } + // Always refresh: even if the selected framework didn't change, alternate packages + // may have been added or removed since the last call. + UpdateAvailableEditors(); + } + + /// + /// Updates the list of editors available across all installed package paths and restores + /// the last preferred editor if it is still available. + /// + /// + /// Each editor (Avalonia, WPF) may live in a different NuGet package directory and + /// a different TFM subfolder (e.g. net10.0 vs net10.0-windows7.0). This + /// method therefore enumerates every tools/<tfm> and lib/<tfm> + /// subdirectory under every path returned by rather than + /// filtering by . The resolved per-editor directory is stored + /// in _editorToDir and consumed by . + /// + private void UpdateAvailableEditors() + { + AvailableEditors.Clear(); + _editorToDir.Clear(); + + var ext = OperatingSystem.IsWindows() ? ".exe" : ".dll"; + foreach (var basePath in GetAllInstalledPaths()) + { + foreach (var toplevelFolder in new[] { "tools", "lib" }) + { + var topDir = Path.Combine(basePath, toplevelFolder); + if (!Directory.Exists(topDir)) + continue; + + foreach (var frameworkDir in Directory.EnumerateDirectories(topDir)) + { + foreach (var name in AllEditorNames()) + { + // First discovery wins: don't overwrite an already-found editor. + if (!_editorToDir.ContainsKey(name) && + File.Exists(Path.Combine(frameworkDir, $"{name}{ext}"))) + { + AvailableEditors.Add(name); + _editorToDir[name] = frameworkDir; + } + } + } + } + } + + UpdateSelectedEditor(); + } + + private static IEnumerable AllEditorNames() + { + yield return GameStudioNames.StrideAvalonia; + if (OperatingSystem.IsWindows()) + yield return GameStudioNames.Stride; + } + + private void UpdateSelectedEditor() + { + var preferred = Launcher.Settings.PreferredEditor; + if (AvailableEditors.Contains(preferred)) + SelectedEditor = preferred; + else + SelectedEditor = AvailableEditors.FirstOrDefault(); } public string PackageSimpleName { get; } @@ -139,7 +220,27 @@ internal void UpdateSelectedFramework() public ObservableList Frameworks { get; } = []; - public string? SelectedFramework { get { return selectedFramework; } set { SetValue(ref selectedFramework, value); } } + public string? SelectedFramework + { + get => selectedFramework; + set + { + if (SetValue(ref selectedFramework, value)) + UpdateAvailableEditors(); + } + } + + /// + /// Gets the editors available for the currently selected framework. + /// Only populated when the version is installed locally. + /// + public ObservableList AvailableEditors { get; } = []; + + /// + /// Gets or sets the editor that will be launched for this version. + /// Reflects and writes back to the global PreferredEditor setting. + /// + public string? SelectedEditor { get => selectedEditor; set => SetValue(ref selectedEditor, value); } /// /// Builds a string that represents the given version numbers. @@ -190,18 +291,18 @@ protected override void UpdateStatus() if (InstallPath is null) return null; - // First, try to use the selected framework - if (SelectedFramework is not null) + // Use the pre-computed editor → directory map from UpdateAvailableEditors. + // Each editor may live in a different package directory and a different TFM subfolder + // (e.g. net10.0 for Avalonia, net10.0-windows7.0 for WPF), so we store the fully + // resolved directory rather than re-applying SelectedFramework here. + foreach (var gameStudioExecutable in GetPreferredExecutableNames()) { - foreach (var toplevelFolder in new[] { "tools", "lib" }) + var editorName = Path.GetFileNameWithoutExtension(gameStudioExecutable); + if (_editorToDir.TryGetValue(editorName, out var dir)) { - var gameStudioDirectory = Path.Combine(InstallPath, toplevelFolder, SelectedFramework); - foreach (var gameStudioExecutable in GetExecutableNames()) - { - var gameStudioPath = Path.Combine(gameStudioDirectory, gameStudioExecutable); - if (File.Exists(gameStudioPath)) - return gameStudioPath; - } + var gameStudioPath = Path.Combine(dir, gameStudioExecutable); + if (File.Exists(gameStudioPath)) + return gameStudioPath; } } @@ -222,6 +323,30 @@ static IEnumerable GetMainExecutables() } } + /// + /// Returns the executable file names in preference order: the selected editor first, then the + /// remaining editors in their default priority order. + /// + private IEnumerable GetPreferredExecutableNames() + { + var ext = OperatingSystem.IsWindows() ? ".exe" : ".dll"; + string[] allNames = OperatingSystem.IsWindows() + ? [GameStudioNames.StrideAvalonia, GameStudioNames.Stride, GameStudioNames.Xenko] + : [GameStudioNames.StrideAvalonia]; + + if (SelectedEditor is not null) + { + yield return $"{SelectedEditor}{ext}"; + foreach (var name in allNames.Where(n => n != SelectedEditor)) + yield return $"{name}{ext}"; + } + else + { + foreach (var name in allNames) + yield return $"{name}{ext}"; + } + } + public int CompareTo(StrideVersionViewModel? other) { var r = Major.CompareTo(other?.Major); diff --git a/sources/launcher/Stride.Launcher/Views/MainView.axaml b/sources/launcher/Stride.Launcher/Views/MainView.axaml index a4867c9e90..4c9b49dfff 100644 --- a/sources/launcher/Stride.Launcher/Views/MainView.axaml +++ b/sources/launcher/Stride.Launcher/Views/MainView.axaml @@ -234,13 +234,27 @@ ItemsSource="{Binding ActiveVersion.Frameworks}" SelectedItem="{Binding ActiveVersion.SelectedFramework}" SelectionChanged="FrameworkChanged" - IsEnabled="{Binding ActiveVersion.Frameworks.Count, Converter={sd:Chained {sd:IsGreater}, Parameter1={sd:Double 1}}}"> + IsEnabled="{Binding !ActiveVersion.IsProcessing, FallbackValue=False}" + IsVisible="{Binding ActiveVersion.Frameworks.Count, Converter={sd:Chained {sd:IsGreater}, Parameter1={sd:Double 1}}, FallbackValue=False}"> + + + + + + + diff --git a/sources/launcher/Stride.Launcher/Views/MainView.axaml.cs b/sources/launcher/Stride.Launcher/Views/MainView.axaml.cs index d760b8726a..ef2a1adb6d 100644 --- a/sources/launcher/Stride.Launcher/Views/MainView.axaml.cs +++ b/sources/launcher/Stride.Launcher/Views/MainView.axaml.cs @@ -25,6 +25,16 @@ private void FrameworkChanged(object? sender, SelectionChangedEventArgs e) } } + private void EditorChanged(object? sender, SelectionChangedEventArgs e) + { + if (DataContext is MainViewModel vm + && EditorSelector.SelectedItem is string editor + && vm.PreferredEditor != editor) + { + vm.PreferredEditor = editor; + } + } + private void VisualStudioDownloadPage_Button_Loaded(object? sender, RoutedEventArgs e) { if (sender is Button button && VisualStudioVersions.AvailableInstances From 5b8f325fe15c73ae7a8a92d8eca18788d2ce6d66 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Fri, 15 May 2026 10:24:25 +0200 Subject: [PATCH 43/46] feat(launcher): restrict editor detection to Avalonia on non-Windows On Linux (and other non-Windows platforms): - CanStart requires the Avalonia editor to be present; Start button stays greyed out for versions that only have a WPF (Stride.GameStudio) package. - UpdateAvailableEditors re-evaluates CanStart after editor discovery, since UpdateStatus runs before AvailableEditors is populated. - EditorNameConverter returns 'Game Studio' without '(Avalonia)' qualifier, as there is no WPF alternative to distinguish from. - TryInstallCompanionsAsync skips the Stride.GameStudio (WPF) companion install on non-Windows platforms. The EditorSelector ComboBox is already hidden when AvailableEditors.Count <= 1, so the dropdown never appears on Linux. --- .../Stride.Launcher/ViewModels/EditorNameConverter.cs | 2 ++ .../ViewModels/StrideStoreVersionViewModel.cs | 4 +++- .../ViewModels/StrideVersionViewModel.cs | 11 ++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs b/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs index 45e86db4c7..f74f9b63a9 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs @@ -16,6 +16,8 @@ public override object Convert(object? value, Type targetType, object? parameter { return (string?)value switch { + // On non-Windows only Avalonia is available; omit the qualifier since there is nothing to distinguish it from. + GameStudioNames.StrideAvalonia when !OperatingSystem.IsWindows() => "Game Studio", GameStudioNames.StrideAvalonia => "Game Studio (Avalonia)", GameStudioNames.Stride => "Game Studio (WPF)", GameStudioNames.Xenko => "Xenko Game Studio", diff --git a/sources/launcher/Stride.Launcher/ViewModels/StrideStoreVersionViewModel.cs b/sources/launcher/Stride.Launcher/ViewModels/StrideStoreVersionViewModel.cs index b721665eac..a558a97e98 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/StrideStoreVersionViewModel.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/StrideStoreVersionViewModel.cs @@ -303,7 +303,9 @@ protected override void UpdateInstallStatus() protected override async Task TryInstallCompanionsAsync(PackageVersion version) { var companionIds = new[] { GameStudioNames.StrideAvalonia, GameStudioNames.Stride } - .Where(id => id != ServerPackage?.Id); + .Where(id => id != ServerPackage?.Id) + // WPF (Stride.GameStudio) is Windows-only; skip on other platforms. + .Where(id => OperatingSystem.IsWindows() || id != GameStudioNames.Stride); foreach (var companionId in companionIds) { diff --git a/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs b/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs index c3afef9b73..8af3230eb5 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs @@ -158,6 +158,14 @@ private void UpdateAvailableEditors() } UpdateSelectedEditor(); + // On non-Windows the Avalonia editor is the only option. Re-evaluate CanStart now + // that AvailableEditors is populated (UpdateStatus runs before UpdateAvailableEditors). + if (!OperatingSystem.IsWindows()) + { + CanStart = CanDelete && AvailableEditors.Count > 0; + if (Launcher.ActiveVersion == this) + Launcher.StartStudioCommand.IsEnabled = CanStart; + } } private static IEnumerable AllEditorNames() @@ -276,7 +284,8 @@ protected override void UpdateStatus() IsVisible = Launcher.ShowBetaVersions || !IsBeta || CanDelete; SetAsActiveCommand.IsEnabled = CanDelete; DeleteCommand.IsEnabled = CanDelete; - CanStart = CanDelete; + // On non-Windows only the Avalonia editor is supported; require it to be present before allowing Start. + CanStart = CanDelete && (OperatingSystem.IsWindows() || AvailableEditors.Count > 0); if (Launcher.ActiveVersion == this) Launcher.StartStudioCommand.IsEnabled = CanStart; From 57ba31f0eb641295e2278af6afde5890bb96683d Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sun, 24 May 2026 20:08:33 +0200 Subject: [PATCH 44/46] Remove Xenko compatibility and beta version toggle from launcher - Drop all Xenko package references (Names.Xenko, GameStudioNames.Xenko, MainPackageIds entries for Xenko/Xenko.GameStudio) since backward compatibility with pre-Stride packages is no longer needed - Simplify PackageFilterExtensions to only match Stride.GameStudio and Stride.GameStudio.Avalonia.Desktop - Remove CheckDeprecatedSourcesCommand (prompted users to add the old Xenko NuGet source) and its AskAddNugetDeprecatedSource string resource - Remove old Xenko executable fallback paths from StrideVersionViewModel - Remove the 'Show/Hide beta versions' toggle button: it existed solely to surface old Xenko (major < 3) packages; with those gone the button had no effect. Removes ShowBetaVersions, IsBeta, IsBetaVersion, the toggle button AXAML, and the ToggleShowBetaVersions/ToggleHideBetaVersions string resources - Update NugetStore.MainPackageIds to include Stride.GameStudio.Avalonia.Desktop - Update launcher README --- .../assets/Stride.Core.Packages/NugetStore.cs | 2 +- sources/launcher/README.md | 2 +- .../Assets/Localization/Strings.Designer.cs | 31 ------------------ .../Assets/Localization/Strings.resx | 13 -------- sources/launcher/Stride.Launcher/Constants.cs | 2 -- .../PackageFilterExtensions.cs | 6 +--- .../ViewModels/EditorNameConverter.cs | 1 - .../ViewModels/MainViewModel.cs | 25 ++------------- .../ViewModels/StrideVersionViewModel.cs | 32 ++----------------- .../Stride.Launcher/Views/MainView.axaml | 20 ------------ 10 files changed, 8 insertions(+), 126 deletions(-) diff --git a/sources/assets/Stride.Core.Packages/NugetStore.cs b/sources/assets/Stride.Core.Packages/NugetStore.cs index 480096d404..0a58c8c985 100644 --- a/sources/assets/Stride.Core.Packages/NugetStore.cs +++ b/sources/assets/Stride.Core.Packages/NugetStore.cs @@ -143,7 +143,7 @@ public static void UpdatePackageSource(ISettings settings, string name, string u /// List of package Ids under which the main package is known. Usually just one entry, but /// we could have several in case there is a product name change. /// - public IReadOnlyCollection MainPackageIds { get; } = ["Stride.GameStudio", "Xenko.GameStudio", "Xenko"]; + public IReadOnlyCollection MainPackageIds { get; } = ["Stride.GameStudio", "Stride.GameStudio.Avalonia.Desktop"]; /// /// Package Id of the Visual Studio Integration plugin. diff --git a/sources/launcher/README.md b/sources/launcher/README.md index 80650c0066..e044174e08 100644 --- a/sources/launcher/README.md +++ b/sources/launcher/README.md @@ -3,7 +3,7 @@ Stride Launcher Source for the Stride Launcher and its Windows installer. -The launcher is the entry point that end users run after installing Stride. It manages the installed Stride/Xenko versions (download, update, uninstall), exposes recent projects, VSIX extensions for Visual Studio, release notes, news, and documentation, and finally starts the selected version of Game Studio. +The launcher is the entry point that end users run after installing Stride. It manages the installed Stride versions (download, update, uninstall), exposes recent projects, VSIX extensions for Visual Studio, release notes, news, and documentation, and finally starts the selected version of Game Studio. It is an [Avalonia](https://avaloniaui.net/) MVVM application, targeting `net10.0` with runtime identifiers `linux-x64` and `win-x64`. It is distributed as a NuGet package (`Stride.Launcher`) and wrapped by an [Advanced Installer](https://www.advancedinstaller.com/) setup on Windows. diff --git a/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs index 440e09f904..eae1170600 100644 --- a/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs +++ b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.Designer.cs @@ -60,19 +60,6 @@ internal Strings() { } } - /// - /// Looks up a localized string similar to If you want to access Xenko 3.0 and lower, we need to add a custom nuget source. - /// - ///Do you want to proceed? - /// - ///This will restart the launcher.. - /// - public static string AskAddNugetDeprecatedSource { - get { - return ResourceManager.GetString("AskAddNugetDeprecatedSource", resourceCulture); - } - } - /// /// Looks up a localized string similar to It seems you do not have any version of Stride currently installed. Would you like to install the latest version?. /// @@ -621,24 +608,6 @@ public static string TabNews { } } - /// - /// Looks up a localized string similar to Click here to hide beta versions. - /// - public static string ToggleHideBetaVersions { - get { - return ResourceManager.GetString("ToggleHideBetaVersions", resourceCulture); - } - } - - /// - /// Looks up a localized string similar to Click here to show beta versions. - /// - public static string ToggleShowBetaVersions { - get { - return ResourceManager.GetString("ToggleShowBetaVersions", resourceCulture); - } - } - /// /// Looks up a localized string similar to Ready. /// diff --git a/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx index e001cedb8a..454c2dcd26 100644 --- a/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx +++ b/sources/launcher/Stride.Launcher/Assets/Localization/Strings.resx @@ -408,12 +408,6 @@ A problem has occurred during the installation of Visual Studio plugin. Check that you have at least one of the compatible version of Visual Studio installed (2012, 2013 or 2015). Messagebox text indicating that the installation of the VSIX has failed - - Click here to hide beta versions - - - Click here to show beta versions - An error occurred while trying to process a developer version: {0} MessageBox text indicating that a dev version could not be processed @@ -440,13 +434,6 @@ {0} Please go to Stride website and download a new version. - - - If you want to access Xenko 3.0 and lower, we need to add a custom nuget source. - -Do you want to proceed? - -This will restart the launcher. Close anyway diff --git a/sources/launcher/Stride.Launcher/Constants.cs b/sources/launcher/Stride.Launcher/Constants.cs index b50f04c2ef..be1bebe20b 100644 --- a/sources/launcher/Stride.Launcher/Constants.cs +++ b/sources/launcher/Stride.Launcher/Constants.cs @@ -7,12 +7,10 @@ internal struct Names { public const string GameStudio = nameof(GameStudio); public const string Stride = nameof(Stride); - public const string Xenko = nameof(Xenko); } internal struct GameStudioNames { public const string Stride = $"{Names.Stride}.{Names.GameStudio}"; public const string StrideAvalonia = $"{Names.Stride}.{Names.GameStudio}.Avalonia.Desktop"; - public const string Xenko = $"{Names.Xenko}.{Names.GameStudio}"; } diff --git a/sources/launcher/Stride.Launcher/PackageFilterExtensions.cs b/sources/launcher/Stride.Launcher/PackageFilterExtensions.cs index 547af57b1b..3f0e049e70 100644 --- a/sources/launcher/Stride.Launcher/PackageFilterExtensions.cs +++ b/sources/launcher/Stride.Launcher/PackageFilterExtensions.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation and Contributors (https://dotnetfoundation.org/ & https://stride3d.net) and Silicon Studio Corp. (https://www.siliconstudio.co.jp) // Distributed under the MIT license. See the LICENSE.md file in the project root for more information. -using Stride.Core; using Stride.Core.Packages; namespace Stride.Launcher; @@ -10,9 +9,6 @@ internal static class PackageFilterExtensions { public static IEnumerable FilterStrideMainPackages(this IEnumerable packages) where T : NugetPackage { - // Stride up to 3.0 package is Xenko, 3.x is Xenko.GameStudio, then Stride.GameStudio - return packages.Where(x => (x.Id is Names.Xenko && x.Version < new PackageVersion(3, 1, 0, 0)) - || (x.Id is GameStudioNames.Xenko && x.Version < new PackageVersion(4, 0, 0, 0)) - || (x.Id is GameStudioNames.Stride or GameStudioNames.StrideAvalonia)); + return packages.Where(x => x.Id is GameStudioNames.Stride or GameStudioNames.StrideAvalonia); } } diff --git a/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs b/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs index f74f9b63a9..0a379fda82 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/EditorNameConverter.cs @@ -20,7 +20,6 @@ public override object Convert(object? value, Type targetType, object? parameter GameStudioNames.StrideAvalonia when !OperatingSystem.IsWindows() => "Game Studio", GameStudioNames.StrideAvalonia => "Game Studio (Avalonia)", GameStudioNames.Stride => "Game Studio (WPF)", - GameStudioNames.Xenko => "Xenko Game Studio", var name => name ?? string.Empty, }; } diff --git a/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs b/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs index 1f0fad1efc..e7003da2f7 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs @@ -39,7 +39,6 @@ public sealed class MainViewModel : DispatcherViewModel, IPackagesLogger, IDispo private bool lastActiveVersionRestored; private AnnouncementViewModel announcement; private bool isVisible; - private bool showBetaVersions; internal ILauncherSettingsService Settings => _settings; @@ -67,22 +66,6 @@ public MainViewModel(IViewModelServiceProvider serviceProvider) await FetchOnlineData(); }); StartStudioCommand = new AnonymousTaskCommand(ServiceProvider, StartStudio) { IsEnabled = false }; - CheckDeprecatedSourcesCommand = new AnonymousTaskCommand(ServiceProvider, async () => - { - var settings = NuGet.Configuration.Settings.LoadDefaultSettings(null); - if (NugetStore.CheckPackageSource(settings, "Stride")) - { - return; - } - // Add Stride package store (still used for Xenko up to 3.0) - if (await ServiceProvider.Get().MessageBoxAsync(Strings.AskAddNugetDeprecatedSource, MessageBoxButton.YesNo, MessageBoxImage.Question) == MessageBoxResult.Yes) - { - NugetStore.UpdatePackageSource(settings, "Stride", "https://packages.stride3d.net/nuget"); - settings.SaveToDisk(); - - SelfUpdater.RestartApplication(); - } - }); foreach (var devVersion in _settings.DeveloperVersions) { @@ -124,8 +107,6 @@ public void Dispose() public IEnumerable StrideVersions => strideVersions; - public bool ShowBetaVersions { get { return showBetaVersions; } set { SetValue(ref showBetaVersions, value); } } - public VsixVersionViewModel VsixPackage2019 { get; } public VsixVersionViewModel VsixPackage2022 { get; } @@ -245,8 +226,6 @@ public int CurrentTab public CommandBase StartStudioCommand { get; } - public CommandBase CheckDeprecatedSourcesCommand { get; } - private async Task FetchOnlineData() { // We ensure that the self-updater task starts once the app is running because it might invoke dialogs. @@ -345,7 +324,7 @@ private async Task FindReferencedPackages(NugetLocalPackage package) foreach (var dependency in package.Dependencies) { string prefix = dependency.Item1.Split('.', 2)[0]; - if (prefix is not "Stride" and not "Xenko") + if (prefix is not "Stride") { continue; } @@ -371,7 +350,7 @@ public async Task RetrieveLocalStrideVersions() var localPackages = await RunLockTask(() => store.GetPackagesInstalled(store.MainPackageIds).FilterStrideMainPackages().OrderByDescending(p => p.Version).ToList()); lock (objectLock) { - // Try to remove unused Stride/Xenko packages after uninstall or update + // Try to remove unused Stride packages after uninstall or update try { Task.WaitAll(RemoveUnusedPackages(localPackages)); diff --git a/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs b/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs index 8af3230eb5..df46c49a04 100644 --- a/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs +++ b/sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs @@ -31,8 +31,6 @@ internal StrideVersionViewModel(MainViewModel launcher, NugetStore store, NugetL Major = major; Minor = minor; SetAsActiveCommand = new AnonymousCommand(ServiceProvider, () => launcher.ActiveVersion = this); - // Update status if the user changes whether to display beta versions. - launcher.PropertyChanged += (s, e) => { if (e.PropertyName == nameof(MainViewModel.ShowBetaVersions)) UpdateStatus(); }; } /// @@ -53,7 +51,6 @@ protected static string[] GetExecutableNames() ? [ $"{GameStudioNames.StrideAvalonia}.exe", $"{GameStudioNames.Stride}.exe", - $"{GameStudioNames.Xenko}.exe", ] : [$"{GameStudioNames.StrideAvalonia}.dll"]; } @@ -211,11 +208,6 @@ private void UpdateSelectedEditor() /// public CommandBase SetAsActiveCommand { get; } - /// - /// Gets whether this version is a beta version. - /// - public bool IsBeta => IsBetaVersion(Major, Minor); - /// /// Gets whether this version should be displayed. /// @@ -259,29 +251,14 @@ public string? SelectedFramework /// A string representing the given version numbers. public static string GetName(string packageSimpleName, int majorVersion, int minorVersion, bool isDisplayName = false) { - if (isDisplayName && IsBetaVersion(majorVersion, minorVersion)) - return $"{packageSimpleName} {majorVersion}.{minorVersion}-beta"; - return $"{packageSimpleName} {majorVersion}.{minorVersion}"; } - /// - /// Indicates if the given version corresponds to a beta version. - /// - /// The major number of the version. - /// The minor nimber of the version. - /// True if the given version is a beta, false otherwise. - public static bool IsBetaVersion(int majorVersion, int minorVersion) - { - return majorVersion < 3; - } - /// protected override void UpdateStatus() { base.UpdateStatus(); - // It is visible if it's installed, or if it's not a beta, or if user want to see be available betas - IsVisible = Launcher.ShowBetaVersions || !IsBeta || CanDelete; + IsVisible = true; SetAsActiveCommand.IsEnabled = CanDelete; DeleteCommand.IsEnabled = CanDelete; // On non-Windows only the Avalonia editor is supported; require it to be present before allowing Start. @@ -324,10 +301,7 @@ static IEnumerable GetMainExecutables() // some old paths used in previous versions if (OperatingSystem.IsWindows()) { - yield return @$"lib\net472\{GameStudioNames.Stride}.exe"; - yield return @$"lib\net472\{GameStudioNames.Xenko}.exe"; - yield return @$"Bin\Windows\{GameStudioNames.Xenko}.exe"; - yield return @$"Bin\Windows-Direct3D11\{GameStudioNames.Xenko}.exe"; + yield return @$"lib\net472\{GameStudioNames.Stride}.exe"; } } } @@ -340,7 +314,7 @@ private IEnumerable GetPreferredExecutableNames() { var ext = OperatingSystem.IsWindows() ? ".exe" : ".dll"; string[] allNames = OperatingSystem.IsWindows() - ? [GameStudioNames.StrideAvalonia, GameStudioNames.Stride, GameStudioNames.Xenko] + ? [GameStudioNames.StrideAvalonia, GameStudioNames.Stride] : [GameStudioNames.StrideAvalonia]; if (SelectedEditor is not null) diff --git a/sources/launcher/Stride.Launcher/Views/MainView.axaml b/sources/launcher/Stride.Launcher/Views/MainView.axaml index 4c9b49dfff..5e19b8b4b6 100644 --- a/sources/launcher/Stride.Launcher/Views/MainView.axaml +++ b/sources/launcher/Stride.Launcher/Views/MainView.axaml @@ -499,26 +499,6 @@ Height="24" Margin="0,2" Opacity="0.5" IsVisible="{Binding IsSynchronizing}" /> - - - - - - - - - - - - - - - - From 2c2c44fff2d3af037b1430a4eb3349febf4b3f99 Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sun, 24 May 2026 20:14:16 +0200 Subject: [PATCH 45/46] docs(launcher): remove Xenko and beta-versions references Update all docs under docs/launcher/ to match the code changes: - cross-platform.md: remove Xenko.GameStudio.exe from Windows fallbacks - lifecycle.md: remove Bin/Windows/Xenko.GameStudio.exe legacy path - README.md: s/Stride\/Xenko versions/Stride versions/ - versions.md: remove 'Beta filter' section (ShowBetaVersions / IsBetaVersion), update StrideStoreAlternateVersionViewModel description, remove Xenko.GameStudio.exe mention, update RemoveUnusedPackages description - viewmodels.md: remove IsBeta from StrideVersionViewModel description, remove 'e.g. legacy Xenko ID' from StrideStoreAlternateVersionViewModel - port-status.md: remove ShowBetaVersions toggle bullet --- docs/launcher/README.md | 2 +- docs/launcher/cross-platform.md | 2 +- docs/launcher/lifecycle.md | 2 +- docs/launcher/port-status.md | 1 - docs/launcher/versions.md | 10 +++------- docs/launcher/viewmodels.md | 4 ++-- 6 files changed, 8 insertions(+), 13 deletions(-) diff --git a/docs/launcher/README.md b/docs/launcher/README.md index 74449b2537..1bf03be65d 100644 --- a/docs/launcher/README.md +++ b/docs/launcher/README.md @@ -1,6 +1,6 @@ # Stride Launcher — Overview -The Stride Launcher is the entry point end users run after installing Stride. It is an Avalonia MVVM application that manages the locally installed Stride/Xenko versions (download, update, uninstall), exposes recent projects and VSIX extensions for Visual Studio, surfaces release notes, news and documentation, and finally starts the selected version of Game Studio. +The Stride Launcher is the entry point end users run after installing Stride. It is an Avalonia MVVM application that manages the locally installed Stride versions (download, update, uninstall), exposes recent projects and VSIX extensions for Visual Studio, surfaces release notes, news and documentation, and finally starts the selected version of Game Studio. The launcher's sources live in [sources/launcher/](../../sources/launcher/). The application itself is [Stride.Launcher](../../sources/launcher/Stride.Launcher/), built against `net10.0` with RIDs `linux-x64` and `win-x64`. It is distributed as a NuGet package (`Stride.Launcher`) and wrapped by an Advanced Installer setup on Windows. diff --git a/docs/launcher/cross-platform.md b/docs/launcher/cross-platform.md index 7c0284b2bc..2f382f321b 100644 --- a/docs/launcher/cross-platform.md +++ b/docs/launcher/cross-platform.md @@ -6,7 +6,7 @@ The launcher is in the middle of a Windows → Avalonia cross-platform port (`xp | Platform | Game Studio file | How it is started | |---|---|---| -| Windows | `Stride.GameStudio.Avalonia.Desktop.exe` (with fallbacks to `Stride.GameStudio.exe`, `Xenko.GameStudio.exe`) | `Process.Start(exe, args)` | +| Windows | `Stride.GameStudio.Avalonia.Desktop.exe` (with fallback to `Stride.GameStudio.exe`) | `Process.Start(exe, args)` | | Linux | `Stride.GameStudio.Avalonia.Desktop.dll` | `Process.Start("dotnet", $"{dll} {args}")` | The choice is in `StrideVersionViewModel.GetExecutableNames` and `MainViewModel.StartStudio`. The switch on `Path.GetExtension(mainExecutable)` decides whether to invoke `dotnet` or the binary directly. diff --git a/docs/launcher/lifecycle.md b/docs/launcher/lifecycle.md index 02c2d5798d..bce4969f62 100644 --- a/docs/launcher/lifecycle.md +++ b/docs/launcher/lifecycle.md @@ -89,7 +89,7 @@ AppBuilder.Configure() Clicking **Start** invokes `MainViewModel.StartStudio(string argument)`: 1. If `AutoCloseLauncher` is on, the launcher prepends `/LauncherWindowHandle {MainViewModel.WindowHandle} ` to the argument string so Game Studio can message it back. -2. `ActiveVersion.LocateMainExecutable()` resolves the path — preferring `{SelectedFramework}` under `tools/` or `lib/`, falling back to legacy paths (`lib/net472/Stride.GameStudio.exe`, `Bin/Windows/Xenko.GameStudio.exe`). +2. `ActiveVersion.LocateMainExecutable()` resolves the path — preferring `{SelectedFramework}` under `tools/` or `lib/`, falling back to the legacy path `lib/net472/Stride.GameStudio.exe`. 3. On `.dll` targets the launcher runs `dotnet `; otherwise it runs the executable directly. `WorkingDirectory` is set to the directory of the executable so `global.json` resolves correctly. 4. The command is disabled for five seconds to debounce double-clicks, then re-enabled if the version is still `CanStart`. 5. The active version is persisted through `LauncherSettings.ActiveVersion`. diff --git a/docs/launcher/port-status.md b/docs/launcher/port-status.md index 4758662fd2..62f7450ae9 100644 --- a/docs/launcher/port-status.md +++ b/docs/launcher/port-status.md @@ -16,7 +16,6 @@ The core is in place: - Self-update flow (NuGet probe → download → file swap → restart). - Recent projects + MRU integration with Game Studio. - VSIX discovery via `VisualStudioVersions` (no-op on Linux, by design). -- `ShowBetaVersions` toggle with Avalonia `Interaction.Behaviors` / `DataTriggerBehavior` (ported correctly). - Recent-project context menu with *Show in Explorer* / *Remove from list* (menu ported; *Show in Explorer* is cross-platform — Windows `explorer.exe /select`, macOS `open -R`, Linux DBus `FileManager1.ShowItems` with `xdg-open` fallback). - Alternate-versions sub-list (ported as a nested `ItemsControl`, no longer a `Popup`). - Localization resx / Urls resx. diff --git a/docs/launcher/versions.md b/docs/launcher/versions.md index 860f976db9..2d5724d5cd 100644 --- a/docs/launcher/versions.md +++ b/docs/launcher/versions.md @@ -8,7 +8,7 @@ Managing Stride versions is the launcher's core responsibility. This file descri flowchart TD SVM["StrideVersionViewModel
(abstract)"] SSV["StrideStoreVersionViewModel
Major.Minor from NuGet feed"] - SSA["StrideStoreAlternateVersionViewModel
Alternate package ID under the same Major.Minor
(e.g. legacy Xenko)"] + SSA["StrideStoreAlternateVersionViewModel
Alternate patch version under the same Major.Minor"] SDV["StrideDevVersionViewModel
Local build or DevRedirect package"] SVM --> SSV @@ -50,7 +50,7 @@ Progress is reported via `IPackagesLogger` — `MainViewModel` implements it and 1. User clicks **Install** or **Update** on a `StrideVersionViewModel`. 2. `PackageVersionViewModel.Download(true)` runs: it sets `IsProcessing`, calls `NugetStore.InstallPackage`, and updates progress via `OnDownloadProgress`. 3. On completion, `UpdateStatus` recomputes `CanBeDownloaded` / `CanDelete` and the UI re-binds. -4. `MainViewModel.RetrieveLocalStrideVersions` is re-run to refresh the version list and to clean up any newly-unused transitive packages via `RemoveUnusedPackages` (walks `Dependencies` starting from the Stride/Xenko main packages and uninstalls anything no longer referenced). +4. `MainViewModel.RetrieveLocalStrideVersions` is re-run to refresh the version list and to clean up any newly-unused transitive packages via `RemoveUnusedPackages` (walks `Dependencies` starting from the Stride main packages and uninstalls anything no longer referenced). 5. `UpdateFrameworks()` re-scans `tools/` and `lib/` for TFM subfolders containing a Game Studio executable (`Stride.GameStudio.Avalonia.Desktop.exe` on Windows, `.dll` on Linux). `SelectedFramework` is restored from `LauncherSettings.PreferredFramework` if present, otherwise the closest match (same `Framework` identifier) is used. ## Uninstall flow @@ -66,10 +66,6 @@ Two entry points: `UninstallHelper` also subscribes to `NugetStore.NugetPackageUninstalling` to close lingering processes before each package is removed — this is why it lives as a disposable member on `MainViewModel` (`uninstallHelper`). -## Beta filter - -`StrideVersionViewModel.IsBetaVersion(major, minor)` returns `true` for `major < 3`. Beta versions are visible only when `MainViewModel.ShowBetaVersions` is on or when the version is already installed. The UI toggle re-raises `UpdateStatus` for every version. - ## Framework selection `StrideVersionViewModel.Frameworks` is an `ObservableList` populated by scanning the package install path. The launcher looks for: @@ -79,7 +75,7 @@ Two entry points: {InstallPath}/lib/{framework}/Stride.GameStudio.Avalonia.Desktop.{exe|dll} ``` -On Windows, `Stride.GameStudio.exe` and legacy `Xenko.GameStudio.exe` are also considered. See `StrideVersionViewModel.GetExecutableNames` and `LocateMainExecutable`. +On Windows, `Stride.GameStudio.exe` is also considered as a fallback. See `StrideVersionViewModel.GetExecutableNames` and `LocateMainExecutable`. ## VSIX diff --git a/docs/launcher/viewmodels.md b/docs/launcher/viewmodels.md index 151278ff40..cbec06eb24 100644 --- a/docs/launcher/viewmodels.md +++ b/docs/launcher/viewmodels.md @@ -76,9 +76,9 @@ Subclasses must implement `Name` and `FullName`, and override `UpdateStatus` to ## Version view models -- [**StrideVersionViewModel**](../../sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs) — abstract base. Adds `Major`/`Minor`, `IsBeta`, `Frameworks` (discovered by scanning `tools/`/`lib/`), `SelectedFramework`, and `LocateMainExecutable()`. `IsBeta` is hard-coded to `major < 3`. +- [**StrideVersionViewModel**](../../sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs) — abstract base. Adds `Major`/`Minor`, `Frameworks` (discovered by scanning `tools/`/`lib/`), `SelectedFramework`, and `LocateMainExecutable()`. - [**StrideStoreVersionViewModel**](../../sources/launcher/Stride.Launcher/ViewModels/StrideStoreVersionViewModel.cs) — an official release. Holds `ReleaseNotes` and `DocumentationPages`, manages the prerequisites installer (`Bin\Prerequisites\install-prerequisites.exe`), and exposes the VSIX packages. -- [**StrideStoreAlternateVersionViewModel**](../../sources/launcher/Stride.Launcher/ViewModels/StrideStoreAlternateVersionViewModel.cs) — a sibling variant (e.g. legacy Xenko ID) that resolves under the same `StrideStoreVersionViewModel`. +- [**StrideStoreAlternateVersionViewModel**](../../sources/launcher/Stride.Launcher/ViewModels/StrideStoreAlternateVersionViewModel.cs) — a sibling patch variant that resolves under the same `StrideStoreVersionViewModel`. - [**StrideDevVersionViewModel**](../../sources/launcher/Stride.Launcher/ViewModels/StrideDevVersionViewModel.cs) — a local build registered via `LauncherSettings.DeveloperVersions` or a `DevRedirect` package. Never downloadable. Marked as "compatible with every project" in the recent projects list. - [**VsixVersionViewModel**](../../sources/launcher/Stride.Launcher/ViewModels/VsixVersionViewModel.cs) — installs/uninstalls the Stride Visual Studio extension in the detected VS instances via `Stride.Core.CodeEditorSupport.VisualStudio.VisualStudioVersions`. From 6d9c72769e678e845cf3c3d8879438227997341b Mon Sep 17 00:00:00 2001 From: Nicolas Musset Date: Sun, 24 May 2026 20:24:35 +0200 Subject: [PATCH 46/46] docs(launcher): rewrite port-status.md for pre-merge review Replace the verbose in-progress tracking doc with a focused reviewer reference. Drop all resolved regression history and done roadmap items. Keep only: - What's ported (brief feature list) - Deliberate changes table (so reviewers don't revert intentional choices) - Open items: .NET 10.0 probe decision + post-merge future work - Cross-references --- docs/launcher/port-status.md | 220 ++++++----------------------------- 1 file changed, 35 insertions(+), 185 deletions(-) diff --git a/docs/launcher/port-status.md b/docs/launcher/port-status.md index 62f7450ae9..3238d3d989 100644 --- a/docs/launcher/port-status.md +++ b/docs/launcher/port-status.md @@ -1,130 +1,28 @@ # Port status: Avalonia branch vs `master` (WPF) -This branch is in the middle of porting the Windows-only WPF launcher (`master`) to a cross-platform Avalonia launcher (`feature/launcher-avalonia-cherrypick`, targeting `net10.0` with `linux-x64` + `win-x64`). [cross-platform.md](cross-platform.md) documents the gaps that are explicitly marked with `TODO` / `FIXME xplat-launcher`. This page is the **complete** delta: features, hooks, visuals, and services that changed between the two branches — including silent regressions that are not flagged anywhere in the code. +The Avalonia port is functionally complete and ready for review. This page captures what a reviewer needs to know: what changed deliberately (so nothing gets reverted by mistake), and what is still open post-merge. -> **Scope.** "Current" = this branch. "Master" = the WPF launcher on `master` at the time of the cherry-pick. Line numbers refer to the current branch unless prefixed with `master:`. +> **Scope.** "Current" = this branch. "Master" = the WPF launcher on `master` at the time of the cherry-pick. -## What's already ported and working +## What's ported -The core is in place: +All core features are in place and working on both Windows and Linux: -- Avalonia 12 MVVM app replacing the WPF UI, with `x:DataType` compiled bindings. +- Avalonia 12 MVVM app with `x:DataType` compiled bindings. - `NugetStore`-backed version discovery, install, uninstall, and update. -- `FileLock`-based cross-platform single-instance mutex (replaces `WindowsMutex`). +- `FileLock`-based cross-platform single-instance mutex. - `EditorPath`-based config locations (cross-platform by construction). -- `MarkView.Avalonia` markdown rendering for release notes / news / docs / announcement, with Mermaid + SVG + TextMate code highlighting. -- Self-update flow (NuGet probe → download → file swap → restart). -- Recent projects + MRU integration with Game Studio. +- `MarkView.Avalonia` markdown rendering (release notes, news, docs, announcements) with Mermaid + SVG + TextMate highlighting. +- Self-update flow (NuGet probe → download → file swap → restart); `force-reinstall` gated to Windows only. +- Recent projects + MRU integration with Game Studio; *Show in Explorer* is cross-platform. - VSIX discovery via `VisualStudioVersions` (no-op on Linux, by design). -- Recent-project context menu with *Show in Explorer* / *Remove from list* (menu ported; *Show in Explorer* is cross-platform — Windows `explorer.exe /select`, macOS `open -R`, Linux DBus `FileManager1.ShowItems` with `xdg-open` fallback). -- Alternate-versions sub-list (ported as a nested `ItemsControl`, no longer a `Popup`). -- Localization resx / Urls resx. -- **Preferred-editor selector**: when both `Stride.GameStudio.Avalonia.Desktop` (Avalonia) and `Stride.GameStudio` (WPF) packages are installed for the same major.minor version, a `ComboBox` appears in the launcher to choose between them. The selection is persisted as `PreferredEditor` in `LauncherSettings.conf`. `StrideVersionViewModel.UpdateAvailableEditors` scans all installed package paths (primary + alternates via `GetAllInstalledPaths`) to find which executables are available; `LocateMainExecutable` routes to the correct per-editor install directory using the `_editorToInstallPath` map. -- **Cross-platform launcher ↔ Game Studio IPC**: on Windows the existing `/LauncherWindowHandle` Win32 HWND path is unchanged; on Linux a named-pipe channel (`/LauncherPipe `) is used instead. See [cross-platform.md](cross-platform.md) § Launcher ↔ GameStudio IPC. +- Preferred-editor selector (`Stride.GameStudio.Avalonia.Desktop` vs `Stride.GameStudio`), persisted as `PreferredEditor`. +- Cross-platform launcher ↔ Game Studio IPC: Win32 HWND on Windows, named pipe on Linux. See [cross-platform.md](cross-platform.md). +- Alternate patch-version sub-list, announcement overlay and release-notes panel with slide animations. +- `HasDoneTask` / `SaveTaskAsDone` persisted under `EditorPath.UserDataPath`. +- Unit tests (`Stride.Launcher.Tests`, 6 passing view-model tests). -## Flagged gaps (already in [cross-platform.md](cross-platform.md)) - -Documented there, recapped for completeness: - -- Prerequisites installer (`install-prerequisites.exe`) and Advanced Installer bundles are Windows-only by construction. - -## Silent regressions (not flagged) - -These are behavioural differences that were not preserved during the port and carry no `TODO` / `FIXME` in the current tree. They compile and appear to work, which is what makes them easy to miss. - -*(Three items previously listed here — `/LauncherWindowHandle` always zero, empty `MainWindow.OnClosing`, and the unpersistent `TabControl.SelectedIndex` — have been resolved. See the 2026-04-22 `feat(launcher): …` commits for the window-lifecycle restoration.)* - -### ~~Recent-project row has an offset hit-test for right-click~~ — Fixed - -~~Observed on Linux during the 2026-04-22 cross-platform-services smoke: right-clicking on a recent-project row does not open the context menu at the row's visible position. The hit-test lands a few pixels lower than the rendered content, so the menu only appears when the user clicks *below* the visible row.~~ - -Fixed (2026-04-27): the `ContextMenu` was attached to the outer `Border` wrapper, not to the `Button` that visually fills the row. On Linux, right-click events on the `Button` did not reliably bubble up to `Border.ContextMenu`. Moved the `ContextMenu` to the `Button` directly and added `VerticalAlignment="Stretch"` so the button fills the full row height — right-click now registers wherever the row is visible. - -### ~~`InstallLatestVersion` fell through when already processing~~ — Fixed (2026-05-02) - -`MainViewModel.InstallLatestVersion` was missing a `return` after showing the "install already in progress" dialog, so `latestVersion.DownloadCommand.Execute()` ran unconditionally even when a download was already running. - -Fixed: added `return` after the dialog + `IsEnabled = false` branch in [MainViewModel.cs](../../sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs). - -### ~~`AnnouncementViewModel` close did not dismiss the overlay~~ — Fixed (2026-05-02) - -`CloseAnnouncement()` set `Validated = true` and optionally saved the "don't show again" flag, but never caused `MainViewModel.Announcement` to be set to `null`. The overlay's `Classes.visible` binding in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml) depends on `Announcement != null`, so clicking **OK** had no visual effect. This was latent only because `DisplayReleaseAnnouncement()` is currently a no-op placeholder. - -Fixed: the `Announcement` setter in [MainViewModel.cs](../../sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs) now subscribes to `AnnouncementViewModel.PropertyChanged` and sets `Announcement = null` when `Validated` becomes `true`. - -### ~~Self-update force-reinstall ran on Linux~~ — Fixed (2026-05-02) - -`SelfUpdater.UpdateLauncherFiles` probed every update package for a `force-reinstall:` line without checking the OS. On Linux this would download a Windows `StrideSetup.exe` and fail at `Process.Start`. The issue was not guarded anywhere despite being flagged as a known gap in [self-update.md](self-update.md) and Phase 4. - -Fixed: the entire force-reinstall block in [SelfUpdater.cs](../../sources/launcher/Stride.Launcher/Services/SelfUpdater.cs) is now wrapped in `if (OperatingSystem.IsWindows())`. On Linux the probe is skipped and the normal in-place file-swap path runs instead. - -### ~~`OpenHyperlinkCommand` lost `.md → .html` rewriting~~ — Fixed (Phase 2) - -~~Master's `Views/Commands.cs`:~~ - -~~Process.Start(new ProcessStartInfo(url.ReplaceLast(".md", ".html")) { UseShellExecute = true });~~ - -Fixed: [App.axaml.cs](../../sources/launcher/Stride.Launcher/App.axaml.cs) `OnLinkClicked` now rewrites URLs ending in `.md` to `.html` before calling `Process.Start`. - -### ~~Announcement overlay lost its slide animation~~ — Fixed (Phase 3) - -~~Master's `Announcement.xaml` wrapped its content in a `Grid` with a `TranslateTransform.X` driven by a `DoubleAnimation` (0.5s, `AccelerationRatio=0.2`, `DecelerationRatio=0.1`) via `Trigger.EnterActions` / `ExitActions` on the `IsEnabled` property. The panel slid in from the right and out to the right.~~ - -~~Current [Announcement.axaml](../../sources/launcher/Stride.Launcher/Views/Announcement.axaml) is a plain `DockPanel` with no transform and no animation — the overlay pops in and out discretely.~~ - -Fixed: the announcement overlay `Border` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml) now uses a `TransformOperationsTransition` (`translateX(0)` ↔ `translateX(100%)`, 1s, `CubicEaseOut`) driven by `Classes.visible`. - -### ~~Release-notes panel lost its slide animation~~ — Fixed (Phase 3) - -~~Master's `LauncherWindow.xaml` had a dedicated right-side column whose visibility was driven by `ActiveReleaseNotes.IsActive` and whose `TranslateTransform.X` was animated with the same 0.5s easing as the announcement. Current release-notes view appears/disappears without animation.~~ - -Fixed: the release-notes `Grid` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml) now uses the same `TransformOperationsTransition` approach, driven by `ActiveReleaseNotes.IsActive`. - -### ~~"Show in Explorer" is hard-wired to `explorer.exe`~~ — Fixed (Phase 1) - -~~[RecentProjectViewModel.cs:72](../../sources/launcher/Stride.Launcher/ViewModels/RecentProjectViewModel.cs#L72):~~ - -```csharp -var startInfo = new ProcessStartInfo("explorer.exe", $"/select,{fullPath.ToOSPath()}") { UseShellExecute = true }; -``` - -~~The menu item is visible on Linux but invocation will fail. Needs a platform switch (`xdg-open {dir}` on Linux, `open -R {path}` on macOS).~~ - -Fixed: `RecentProjectViewModel.Explore` has a full platform switch — Windows `explorer.exe /select`, macOS `open -R`, Linux DBus `org.freedesktop.FileManager1.ShowItems` with `xdg-open {parent-dir}` fallback. See [cross-platform.md](cross-platform.md) § Recent-project "Show in Explorer". - -### `SiliconStudioStrideDir` / `StrideDir` env vars no longer seeded - -Master's `Launcher.cs` prepared these environment variables so legacy Stride ≤ 3.0 Game Studio builds could resolve the install root. Removed on the current branch. Probably acceptable given Stride 2.x / 3.0 is EOL, but worth an explicit decision. - -### ~~`CurrentToolTip` shared status-line behavior~~ — Already present - -~~Master had a `BindCurrentToolTipStringBehavior` wired on every control that updated a shared `MainViewModel.CurrentToolTip` property on hover (a status-bar-style "explain what this control does" line). The binding and the property are both gone from the current branch.~~ - -`BindCurrentToolTipStringBehavior` is implemented in `Stride.Core.Presentation.Avalonia.Behaviors` and is already wired on every relevant control in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml). `MainViewModel.CurrentToolTip` is present and bound in the status bar. Not a regression. - -### ~~`StaysOpenContextMenu` / alternate-versions `Popup`~~ — Already resolved - -~~Master rendered alternate versions in a `Popup` + `ToggleButton` with a custom `StaysOpenContextMenu` class so the popup didn't close on item clicks. Current branch renders them as a nested `ItemsControl` directly in the main list.~~ - -The current branch does use a `ToggleButton` + `Popup` with `IsLightDismissEnabled="True"`. An `EventTriggerBehavior` on each item's `Click` event fires `ChangePropertyAction` to set `Popup.IsOpen = False`, which replicates the `StaysOpenContextMenu` behaviour in Avalonia. Not a regression. - -## Features removed without a replacement - -These are deletions from master that carry no replacement code on this branch. - -### `PrerequisitesValidator` - -- Master's `Program.Main` called `PrerequisitesValidator.Validate(args)` before `Launcher.Main`. That class probed `HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full` for a release key ≥ `461808` (.NET 4.7.2), and if missing, ran `launcher-prerequisites.exe`, waited for exit, and restarted the launcher. -- Current branch has no equivalent. The check was .NET-4.7.2-specific and is obsolete now, but there is no replacement probe for .NET 10.0 runtime availability. On a Windows box without the right runtime, the launcher will fail to start with a platform-level error rather than a friendly prompt. -- On Linux this check doesn't translate — .NET is installed via the package manager — so documenting "Windows self-contained publish handles this" may be all that is needed. - -### `MinimalApp` crash-report / already-running dialog split - -Master had a dedicated `MinimalApp` WPF `Application` instance used for the crash-report dialog and the "another instance is already running" dialog, so those paths did not spin up the full launcher UI. Current branch has a `MinimalApp : App` class at the bottom of [App.axaml.cs](../../sources/launcher/Stride.Launcher/App.axaml.cs) whose `OnFrameworkInitializationCompleted` is intentionally empty. - -This is by design: both dialog paths (`CrashReport` and `DisplayError` in [Launcher.cs](../../sources/launcher/Stride.Launcher/Launcher.cs)) construct and show their window directly inside their own `AppMain` callback, setting `DataContext` and wiring `Closed` themselves. `MinimalApp` therefore does not need to create a `MainViewModel`, initialise the markdown pipeline, or provide any services — skipping all of that is the point. Not a regression. - -## Deliberate changes (for reference, not roadmap items) +## Deliberate changes Captured here so reviewers don't try to "revert" them: @@ -136,82 +34,34 @@ Captured here so reviewers don't try to "revert" them: | Presentation lib | `Stride.Core.Presentation.Wpf` | `Stride.Core.Presentation.Avalonia` | Avalonia port | | Markdown | Markdig via WPF renderer | `MarkView.Avalonia` | Avalonia port | | Telemetry | `Stride.Metrics` / `MetricsClient` wrapping the run, `MetricsHelper.NotifyDownload*` around every package op | Removed | **Intentional, permanent** — the xplat launcher does not ship telemetry | -| Privacy policy | `PrivacyPolicyHelper.EnsurePrivacyPolicyStride40()` on startup, `RevokeAllPrivacyPolicy()` on uninstall | Removed | **Intentional, permanent** — the consent flow is no longer needed since telemetry is gone. No uninstall cleanup is performed; any residual registry keys / settings files from the old WPF launcher are harmless orphans | - -## Roadmap - -Ordered by blast radius — each phase is useful on its own. - -### Phase 1 — unblock daily use on both platforms - -These change observable behaviour on both Windows and Linux and should ship first. - -1. ~~**Wire `WindowHandle` on Avalonia `MainWindow`.**~~ **Done** (2026-04-22): HWND captured in `MainWindow.OnOpened` on Windows via `TryGetPlatformHandle()`; stays `IntPtr.Zero` on Linux until xplat-GameStudio lands. See [cross-platform.md](cross-platform.md) § Launcher ↔ GameStudio IPC. -2. ~~**Restore `MainWindow.OnClosing`.**~~ **Done** (2026-04-22): confirmation dialog with `Close anyway` / `Keep launcher open` buttons shown when any version `IsProcessing`; `LauncherSettings.ActiveVersion` persisted on close via `MainViewModel.TryCloseAsync`. -3. ~~**Persist `LauncherSettings.CurrentTab` on tab change.**~~ **Done** (2026-04-22): `MainViewModel.CurrentTab` persisted-on-set, two-way bound from the `TabControl`. -4. ~~**Port `HasDoneTask` / `SaveTaskAsDone` to a file under `EditorPath.UserDataPath`.**~~ **Done** (2026-04-22): one-shot task state moved into `Internal/Launcher/CompletedTasks` inside the existing `LauncherSettings.conf`. No migration — pre-existing `HKCU\SOFTWARE\Stride\` keys on Windows become harmless orphans. -5. ~~**Fix `explorer.exe` hard-coding** in `RecentProjectViewModel.Explore`.~~ **Done** (2026-04-22): platform switch — Windows `explorer.exe /select`, macOS `open -R`, Linux DBus `FileManager1.ShowItems` with `xdg-open` fallback. See [cross-platform.md](cross-platform.md) § Recent-project "Show in Explorer". -6. ~~**Cross-platform launcher ↔ Game Studio IPC.**~~ **Done** (2026-05-09): on Linux the HWND channel is replaced by a named pipe. `MainViewModel.StartStudio` injects `/LauncherPipe ` on non-Windows and starts `WaitForGameStudioPipeSignalAsync`; `Program.ParseLauncherArgs` in `Stride.GameStudio.Avalonia.Desktop` handles both `/LauncherWindowHandle` (Windows) and `/LauncherPipe` (Linux). `App.LauncherNotifier` fires once from `MainWindow.OnLoaded`. See [cross-platform.md](cross-platform.md) § Launcher ↔ GameStudio IPC. - -### Phase 2 — feature restoration - -1. ~~**Restore `.md → .html` URL rewriting** in the `OnLinkClicked` handler in [App.axaml.cs](../../sources/launcher/Stride.Launcher/App.axaml.cs) before calling `Process.Start`.~~ **Done** (2026-04-27): `OnLinkClicked` now rewrites `.md` → `.html` before `Process.Start`. -2. **Decide on `.NET 10.0` runtime probe for Windows.** Either embed it as a self-contained publish (no probe needed), or add a small `PrerequisitesValidator` replacement that checks the runtime and surfaces a friendly message. Document the decision in [packaging.md](packaging.md). -3. ~~**Review `MinimalApp` paths.**~~ **Closed (2026-05-02):** `MinimalApp.OnFrameworkInitializationCompleted` is intentionally empty. The crash-report and already-running-instance paths in [Launcher.cs](../../sources/launcher/Stride.Launcher/Launcher.cs) construct and show their window directly inside the `AppMain` callback, so no framework-level initialisation is needed. Not a regression. -4. ~~**Migration cleanup for users upgrading from the WPF launcher.**~~ **Closed (2026-05-02):** decided not to clean up legacy privacy-policy / telemetry state on uninstall. Telemetry was removed entirely, so any residual registry keys or settings files from the old WPF launcher are harmless orphans. The commented-out `RevokeAllPrivacyPolicy` placeholder in `Launcher.cs` has been deleted. - -### Phase 3 — visual parity - -Nice-to-have UX polish. The entries below all map to Avalonia `Transitions` on the relevant `TranslateTransform.X` / `Opacity`, which is how animations are expressed in Avalonia (versus WPF's `Storyboard`/`DoubleAnimation`). - -1. ~~**Announcement slide-in/out**~~ **Done**: `TransformOperationsTransition` on `RenderTransform` (`translateX(0)` ↔ `translateX(100%)`), 1s, `CubicEaseOut`. Implemented via CSS-class binding (`Classes.visible`) on the overlay `Border` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml). Root `Grid` gained `ClipToBounds="True"` to prevent the panel from showing while off-screen. -2. ~~**Release-notes panel slide**~~ **Done**: Same approach — `TransformOperationsTransition` on the release-notes `Grid` in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml), bound to `ActiveReleaseNotes.IsActive`, 1s, `CubicEaseOut`. The main-content grid's `IsVisible` binding was replaced with `IsEnabled`-only (layout stays; the release-notes panel renders on top while sliding in). -3. ~~**`CurrentToolTip` status-line behavior**~~ **Already done** (pre-existing): `BindCurrentToolTipStringBehavior` is implemented in `Stride.Core.Presentation.Avalonia` and wired on every relevant control in [MainView.axaml](../../sources/launcher/Stride.Launcher/Views/MainView.axaml). `MainViewModel.CurrentToolTip` property also present. -4. ~~**Optional: revisit alternate-versions UX.**~~ **Already done** (pre-existing): The current branch uses a `ToggleButton` + `Popup` with `IsLightDismissEnabled="True"` and an `EventTriggerBehavior` that closes the popup on item click — functionally equivalent to the master `StaysOpenContextMenu` approach. - -### Phase 4 — platform expansion - -Not required for parity, but on the horizon: - -1. **Linux packaging.** `dotnet publish -r linux-x64 --self-contained` produces a tree today. Decide on distribution format: tarball, AppImage, Flatpak, or `.deb` / `.rpm`. Update [packaging.md](packaging.md). -2. **macOS support.** No RID yet. Needs `osx-x64` / `osx-arm64` targets, `.app` bundle layout, codesigning / notarization, and window chrome review. -3. **Self-update on Linux.** The `force-reinstall` path is now gated to `OperatingSystem.IsWindows()` (fixed 2026-05-02), so Linux no longer attempts to download `StrideSetup.exe`. The normal in-place file-swap path runs on Linux, but a full `force-reinstall` (breaking-change upgrade) has no Linux equivalent yet. Options: (a) a Linux-only code path that downloads the matching `.tar.gz` / AppImage and replaces the install in place, or (b) "update via your package manager" documented as the intended path. Mentioned in [self-update.md](self-update.md) and [cross-platform.md](cross-platform.md). - -### Phase 5 — test infrastructure - -The launcher has no unit or integration tests today. Bootstrap a test project for the launcher, leveraging Avalonia's headless-platform support so tests can exercise real views (bindings, commands, dialogs, keyboard/mouse input) without a display server. - -1. ~~**Bootstrap `Stride.Launcher.Tests`.**~~ **Done** (2026-04-27): `Stride.Launcher.Tests.csproj` created under `sources/launcher/Stride.Launcher.Tests/`, using xUnit, added to `Stride.Launcher.sln`. Helpers: `InMemoryLauncherSettings`, `FakeDialogService`, `FakeDispatcherService`, `TestViewModelFactory`. To enable internal access, `Stride.Launcher.csproj` uses ``. - - **Prerequisite — `ILauncherSettingsService`** (2026-04-27): introduced to break the direct `LauncherSettings.*` static coupling that would otherwise make view-model tests impossible. `LauncherSettingsService` wraps the real settings; all usages in `MainViewModel`, `AnnouncementViewModel`, `StrideVersionViewModel`, and `MainView.axaml.cs` have been migrated. `MainViewModel` gained an internal test constructor that accepts both `IViewModelServiceProvider` and `ILauncherSettingsService`, skipping NuGet/network/file-system initialisation. - -2. ~~**View-model tests** (partial).~~ **Done** (2026-04-27): 6 tests in [MainViewModelTests.cs](../../sources/launcher/Stride.Launcher.Tests/MainViewModelTests.cs), all passing: - - `HasDoneTask` returns `false` before any task is recorded, `true` after `SaveTaskAsDone`. - - `SaveTaskAsDone` is idempotent (does not double-save). - - `CurrentTab` setter persists the value and calls `Save()` exactly once; no save on no-change. - - `TryCloseAsync` returns `true`, does not invoke the dialog, and saves settings when no version is processing. +| Privacy policy | `PrivacyPolicyHelper.EnsurePrivacyPolicyStride40()` on startup, `RevokeAllPrivacyPolicy()` on uninstall | Removed | **Intentional, permanent** — consent flow no longer needed; residual registry keys / settings from the old WPF launcher are harmless orphans | +| Xenko backward compat | `NugetStore.MainPackageIds` included `"Xenko.GameStudio"` and `"Xenko"`; `PackageFilterExtensions` matched Xenko version ranges; `StrideVersionViewModel` searched for `Xenko.GameStudio.exe` and old `Bin\Windows\` paths; `CheckDeprecatedSourcesCommand` prompted to add the legacy NuGet source | Removed | Xenko is EOL; only Stride 4+ packages are supported | +| Beta version filter | `ShowBetaVersions` toggle; `IsBeta` / `IsBetaVersion(major, minor)` (`true` for `major < 3`) | Removed | "Beta" versions were Xenko packages; with Xenko dropped every installed version is always visible | +| `SiliconStudioStrideDir` / `StrideDir` env vars | Seeded by `Launcher.cs` so Stride ≤ 3.0 Game Studio could resolve the install root | Not seeded | Stride ≤ 3.0 / Xenko support dropped | +| `PrerequisitesValidator` | Probed `HKLM` for .NET 4.7.2; launched `launcher-prerequisites.exe` if missing | Removed | .NET 4.7.2 check is obsolete; see open item below | - Deferred: `TryCloseAsync` keep-open / close-anyway branches require constructing a `StrideVersionViewModel` with a real `NugetStore` — tracked as follow-up. +## Open items -3. **Avalonia headless integration tests.** Exercise the real `MainWindow` + `MainView`: - - `Opened` fires → `MainViewModel.WindowHandle` is non-zero on Windows, zero on Linux. - - `TabControl` selection change propagates to `LauncherSettings.CurrentTab`. - - Simulated close with in-progress download shows the confirmation dialog. -4. **CI wiring.** Run the test project on both `windows-latest` and `ubuntu-latest` in the existing launcher build workflow so platform divergences surface early. +### Pending decision: .NET 10.0 runtime probe (Windows) -This phase is not blocked by the others — it can be pulled earlier any time new behaviour needs coverage. Each Phase 1–3 item whose surface is naturally testable should note the tests that will be added here so nothing is forgotten. +The old `PrerequisitesValidator` has no replacement. On a Windows machine without .NET 10.0 the launcher fails at the OS level with no user-friendly message. Options: -## Beyond parity (proposed enhancements) +- **Self-contained publish** — bundle the runtime; no probe needed. Document in [packaging.md](packaging.md). +- **Lightweight check** — detect missing runtime early and surface a download link before crashing. -Items that were never in the master WPF launcher but are reasonable next steps once parity is achieved. They are not on any phase above because "do nothing" is a valid answer — only pick them up if they become a real pain point. +### Future work (post-merge) -1. **Persist the selected alternate version.** The `Internal/Launcher/ActiveVersion` setting stores only `"Stride ."` (see [StrideVersionViewModel.GetName](../../sources/launcher/Stride.Launcher/ViewModels/StrideVersionViewModel.cs#L152)). When a user has multiple patch-level builds of the same major.minor installed (e.g., `4.3.0.1` and `4.3.0.2`), the launcher remembers which *major.minor* was active but always selects the default patch on restart. Requires: extending the setting format to capture the full version (or adding a sibling key `ActiveAlternateVersion`), and updating the restore logic in [MainViewModel.RetrieveLocalStrideVersions](../../sources/launcher/Stride.Launcher/ViewModels/MainViewModel.cs#L355) to consult it. Surfaced during the 2026-04-22 window-lifecycle smoke. +- **Linux packaging.** `dotnet publish -r linux-x64 --self-contained` works today. Decide on distribution format (tarball, AppImage, Flatpak, `.deb`/`.rpm`) and document in [packaging.md](packaging.md). +- **macOS support.** No RID yet. Needs `osx-x64`/`osx-arm64` targets, `.app` bundle, codesigning/notarization, and window-chrome review. +- **Self-update force-reinstall on Linux.** The `force-reinstall` path is Windows-only. A full breaking-change upgrade on Linux has no equivalent yet — options are a Linux-specific download path or "update via package manager". See [self-update.md](self-update.md). +- **Integration tests.** The existing unit tests cover view-model logic only. Avalonia headless-platform tests for the real `MainWindow` + `MainView` (close-confirmation dialog, tab persistence, HWND capture) are still missing, as is CI wiring on `ubuntu-latest`. +- **Persist the selected alternate version.** `ActiveVersion` stores only `"Stride ."`; the active patch build is not remembered across restarts. Extend the setting or add `ActiveAlternateVersion` and update the restore logic in `MainViewModel.RetrieveLocalStrideVersions`. ## Cross-references -- [cross-platform.md](cross-platform.md) — the "which OS does what" view; update alongside this page as gaps close. -- [lifecycle.md](lifecycle.md) — entry point, `/LauncherWindowHandle` argument, close flow. +- [cross-platform.md](cross-platform.md) — per-OS code paths and remaining gaps. +- [lifecycle.md](lifecycle.md) — entry point, close flow, Game Studio launch. - [viewmodels.md](viewmodels.md) — `MainViewModel` structure and commands. -- [views.md](views.md) — XAML views, converters. +- [views.md](views.md) — AXAML views and converters. - [self-update.md](self-update.md) — force-reinstall flow. -- [packaging.md](packaging.md) — Advanced Installer projects, NuGet package, target RIDs. +- [packaging.md](packaging.md) — NuGet package, Advanced Installer, target RIDs.