diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 4954334..a003d66 100644 --- a/installer/PSLauncher.iss +++ b/installer/PSLauncher.iss @@ -11,7 +11,7 @@ #define MyAppName "PROSERVE Launcher" #define MyAppShortName "PS_Launcher" -#define MyAppVersion "1.0.5" +#define MyAppVersion "1.0.6" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index 748edba..20e5ecb 100644 --- a/src/PSLauncher.App/PSLauncher.App.csproj +++ b/src/PSLauncher.App/PSLauncher.App.csproj @@ -18,9 +18,9 @@ PROSERVE Launcher © 2026 ASTERION VR — All rights reserved PSLauncher.App - 1.0.5 - 1.0.5.0 - 1.0.5.0 + 1.0.6 + 1.0.6.0 + 1.0.6.0 true diff --git a/src/PSLauncher.App/ViewModels/MainViewModel.cs b/src/PSLauncher.App/ViewModels/MainViewModel.cs index 135a612..6644782 100644 --- a/src/PSLauncher.App/ViewModels/MainViewModel.cs +++ b/src/PSLauncher.App/ViewModels/MainViewModel.cs @@ -557,8 +557,24 @@ public sealed partial class MainViewModel : ObservableObject : rawRemote.Where(v => !v.IsBeta).ToList(); var remoteByVer = remote.ToDictionary(v => v.Version); + // Tri combiné installé + remote. On utilise VersionOrder (SemVer + isBeta) + // pour respecter la règle « non-beta > beta au même préfixe 3-digit » : + // sans ça, une install de 1.5.4.32 beta resterait "featured" après la + // publication de 1.5.4 non-beta. + // + // IMPORTANT : on lookupe isBeta dans `rawRemote` (non filtré) et PAS dans + // `remoteByVer` (filtré par canSeeBetas). Sinon un client sans droits beta + // qui aurait installé 1.5.4.32 beta ne verrait plus l'info isBeta pour + // cette entrée (elle a été retirée par le filtre) → VersionOrder retomberait + // sur SemVer strict → 1.5.4.32 > 1.5.4 → featured resterait sur la beta + // installée. En regardant `rawRemote` on garde l'info d'origine. + var rawBetaByVer = rawRemote + .GroupBy(v => v.Version) + .ToDictionary(g => g.Key, g => g.Any(v => v.IsBeta)); + bool IsBetaOf(string ver) => rawBetaByVer.TryGetValue(ver, out var isBeta) && isBeta; var allVersions = installed.Keys.Union(remoteByVer.Keys) - .OrderByDescending(v => SemVer.Parse(v)) + .OrderByDescending(v => v, Comparer.Create((a, b) => + VersionOrder.Compare(a, IsBetaOf(a), b, IsBetaOf(b)))) .ToList(); var rows = new List(); diff --git a/src/PSLauncher.Core/Installations/VersionOrder.cs b/src/PSLauncher.Core/Installations/VersionOrder.cs new file mode 100644 index 0000000..08a4d4a --- /dev/null +++ b/src/PSLauncher.Core/Installations/VersionOrder.cs @@ -0,0 +1,55 @@ +using PSLauncher.Models; + +namespace PSLauncher.Core.Installations; + +/// +/// Comparaison de versions PROSERVE qui prend en compte le flag isBeta +/// du manifest en plus du SemVer numérique. +/// +/// Règle : au même préfixe 3-digit X.Y.Z, une version isBeta=false +/// est considérée SUPÉRIEURE à une version isBeta=true, quel que soit le +/// 4ᵉ digit. Cas d'usage typique : après avoir itéré des builds beta 1.5.4.30 +/// .31 .32, l'opérateur publie 1.5.4 non-beta comme release finale. +/// Sous SemVer pur, 1.5.4 == 1.5.4.0 < 1.5.4.32 donc le launcher +/// resterait pointé sur la beta ; ce helper inverse la comparaison pour ce +/// cas précis afin que la release finale soit considérée comme LA version +/// courante. +/// +/// Hors ce cas (préfixes 3-digit différents, ou même statut beta des deux +/// côtés), on retombe sur — donc : +/// +/// 1.5.4 (non-beta) vs 1.5.4.32 (beta) → 1.5.4 gagne (règle spéciale) +/// 1.5.4.30 (beta) vs 1.5.4.32 (beta) → 1.5.4.32 gagne (SemVer) +/// 1.5.4 (non-beta) vs 1.5.5.10 (beta) → 1.5.5.10 gagne (SemVer, préfixes différents) +/// 1.5.4 vs 1.5.5 → 1.5.5 gagne (SemVer) +/// +/// +public static class VersionOrder +{ + /// + /// Retourne <0 si A < B, 0 si égales, >0 si A > B. Voir la doc de + /// pour la règle isBeta. + /// + public static int Compare(string versionA, bool isBetaA, string versionB, bool isBetaB) + { + var svA = SemVer.Parse(versionA); + var svB = SemVer.Parse(versionB); + + // Cas spécial : même préfixe 3-digit ET statut beta différent. + // La version non-beta l'emporte, indépendamment du 4ᵉ digit (traité + // comme un suffixe pré-release dans ce contexte particulier). + if (svA.Major == svB.Major + && svA.Minor == svB.Minor + && svA.Patch == svB.Patch + && isBetaA != isBetaB) + { + return isBetaA ? -1 : 1; + } + + return svA.CompareTo(svB); + } + + /// Overload pratique pour une . + public static int Compare(VersionManifest a, VersionManifest b) + => Compare(a.Version, a.IsBeta, b.Version, b.IsBeta); +} diff --git a/src/PSLauncher.Core/Updates/UpdateChecker.cs b/src/PSLauncher.Core/Updates/UpdateChecker.cs index 4591cc2..87cfb52 100644 --- a/src/PSLauncher.Core/Updates/UpdateChecker.cs +++ b/src/PSLauncher.Core/Updates/UpdateChecker.cs @@ -28,22 +28,41 @@ public sealed class UpdateChecker : IUpdateChecker var manifest = await _manifestService.FetchAsync(ct).ConfigureAwait(false); // On ignore le champ `manifest.Latest` (trop facile à oublier au serveur). - // On prend toujours la plus haute version SemVer disponible et téléchargeable. + // On prend toujours la plus haute version disponible et téléchargeable, en + // ordonnant via VersionOrder — ça respecte la règle « non-beta > beta au + // même préfixe 3-digit » (ex : 1.5.4 non-beta > 1.5.4.32 beta), sinon + // c'est du SemVer strict. var latest = manifest.Versions .Where(v => v.AvailableForDownload) - .OrderByDescending(v => SemVer.Parse(v.Version)) + .OrderByDescending(v => v, Comparer.Create(VersionOrder.Compare)) .FirstOrDefault(); var installed = _registry.Scan(); + // Pour les installs locales on ne connaît pas leur statut beta d'origine + // (l'entrée manifest a pu disparaître entre-temps). On les compare en + // SemVer pur — c'est acceptable parce que le vrai check « faut-il updater » + // ci-dessous confronte l'install LA PLUS HAUTE au meilleur remote, qui + // lui embarque son isBeta. var latestInstalled = installed .OrderByDescending(v => SemVer.Parse(v.Version)) .FirstOrDefault(); var isLatestInstalled = latest is not null && latestInstalled is not null && latestInstalled.Version == latest.Version; + // Pour « isNewer », on doit décider si `latest` (remote) supersede + // `latestInstalled` (local). On cherche l'entrée manifest correspondante + // à l'install locale pour retrouver son isBeta d'origine. Si absente + // (typiquement : install locale d'une ancienne beta que l'opérateur a + // supprimée du manifest), on la traite comme non-beta pour ne pas + // fausser la comparaison en défaveur de l'install locale. + var installedRemote = latestInstalled is null + ? null + : manifest.Versions.FirstOrDefault(v => v.Version == latestInstalled.Version); + var installedIsBeta = installedRemote?.IsBeta ?? false; var isNewer = latest is not null && (latestInstalled is null - || SemVer.Parse(latest.Version).CompareTo(SemVer.Parse(latestInstalled.Version)) > 0); + || VersionOrder.Compare(latest.Version, latest.IsBeta, + latestInstalled.Version, installedIsBeta) > 0); return new UpdateCheckResult(manifest, latest, latestInstalled, isLatestInstalled, isNewer, null); }