diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 6f9e4b4..0906c55 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.10" +#define MyAppVersion "1.0.11" #define MyAppPublisher "ASTERION VR" #define MyAppURL "https://asterionvr.com" #define MyAppExeName "PS_Launcher.exe" diff --git a/server/api/routes/Manifest.php b/server/api/routes/Manifest.php index c57f6e6..bd2e311 100644 --- a/server/api/routes/Manifest.php +++ b/server/api/routes/Manifest.php @@ -40,9 +40,17 @@ final class Manifest $clientChannel = null; } + // Multi-channel opt-in : les clients v1.0.10+ savent afficher plusieurs + // entries au même numéro de version (channels distincts). Ils passent + // ?multiChannel=1 pour signaler la capabilité. Les clients v1.0.9- (et + // avant refactor row-by-Id) crasheraient sur ToDictionary(v.Version) → + // le serveur reste en mode dédup pour eux (comportement historique). + $multiChannel = !empty($_GET['multiChannel']); + $manifest['versions'] = self::filterVersions( $manifest['versions'] ?? [], - $clientChannel + $clientChannel, + $multiChannel ); // Re-signature à la volée. La clé privée Ed25519 est dans config.php @@ -85,7 +93,7 @@ final class Manifest * @param array> $versions * @return list> */ - private static function filterVersions(array $versions, ?string $clientChannel): array + private static function filterVersions(array $versions, ?string $clientChannel, bool $multiChannel = false): array { // Étape 1 : filter visible par ce client $visible = []; @@ -102,7 +110,18 @@ final class Manifest } } - // Étape 2 : group by version, pick most specific per group + // Client v1.0.10+ : bypass la dédup, retourne toutes les entries visibles. + // Le client sait maintenant afficher plusieurs rows au même numéro (keyage + // par entryId côté RebuildList) et bloque les collisions d'install côté + // guard. Le badge channel n'apparaît que si plusieurs entries partagent + // le numéro (v1.0.9+) → UX propre. + if ($multiChannel) { + return array_map(fn($item) => $item['entry'], $visible); + } + + // Étape 2 (clients v1.0.9-) : group by version, pick most specific per + // group. Comportement historique pour ne pas crasher les vieux clients + // qui font ToDictionary(v.Version) et exploseraient sur des duplicates. $byVersion = []; foreach ($visible as $item) { $key = (string)($item['entry']['version'] ?? '?'); diff --git a/src/PSLauncher.App/PSLauncher.App.csproj b/src/PSLauncher.App/PSLauncher.App.csproj index b6f553e..6288bb5 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.10 - 1.0.10.0 - 1.0.10.0 + 1.0.11 + 1.0.11.0 + 1.0.11.0 true diff --git a/src/PSLauncher.Core/Manifests/ManifestService.cs b/src/PSLauncher.Core/Manifests/ManifestService.cs index 22e87f0..2569d9d 100644 --- a/src/PSLauncher.Core/Manifests/ManifestService.cs +++ b/src/PSLauncher.Core/Manifests/ManifestService.cs @@ -146,9 +146,16 @@ public sealed class ManifestService : IManifestService // Whitelist côté client aussi pour éviter des chars exotiques qui casseraient l'URL. var channel = _channelProvider(); var baseUrl = TrimSlash(_serverBaseUrlProvider()) + "/manifest"; - var url = !string.IsNullOrWhiteSpace(channel) && System.Text.RegularExpressions.Regex.IsMatch(channel!, "^[a-z0-9_-]{1,64}$") - ? $"{baseUrl}?channel={Uri.EscapeDataString(channel!)}" - : baseUrl; + // Query params : ?channel=X (filtrage par channel license) + &multiChannel=1 + // (opt-in au comportement v1.0.10+ : le serveur envoie plusieurs entries au + // même numéro de version sans dédup, le client sait afficher chacune sur + // sa row). Toujours envoyé — les serveurs anciens l'ignorent silencieusement. + var qs = new System.Text.StringBuilder("?multiChannel=1"); + if (!string.IsNullOrWhiteSpace(channel) && System.Text.RegularExpressions.Regex.IsMatch(channel!, "^[a-z0-9_-]{1,64}$")) + { + qs.Append("&channel=").Append(Uri.EscapeDataString(channel!)); + } + var url = baseUrl + qs.ToString(); using var ovhCts = CancellationTokenSource.CreateLinkedTokenSource(ct); ovhCts.CancelAfter(OvhFetchTimeoutMs); using var req = new HttpRequestMessage(HttpMethod.Get, url);