From ff05edbe7efef7f1513ddd37ef15e69ce3b9b9e0 Mon Sep 17 00:00:00 2001 From: "j.foucher" Date: Mon, 6 Jul 2026 14:55:10 +0200 Subject: [PATCH] =?UTF-8?q?v1.0.4=20=E2=80=94=20Conserver=20les=20sauvegar?= =?UTF-8?q?des=20:=20user-data=20wins=20over=20ZIP=20defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug rapporté par l'opérateur : upgrade 1.5.4.30 → 1.5.4.32 sur un poste, les paramètres customisés dans 1.5.4.30 n'étaient pas visibles au premier lancement de 1.5.4.32 alors que la case « Conserver les sauvegardes » était cochée. Root cause : le ZIP de la nouvelle version bundle des .sav par défaut (typ. PROSERVE_UE_5_7/Saved/SaveGames/GeneralSettings.sav = réglages usine). Après extraction, ce fichier existe déjà dans le dossier cible. Ma logique « non-destructive » précédente skippait alors la copie depuis la version précédente pour « ne pas écraser un fichier bundlé » — mais c'est exactement l'inverse qu'on veut : les données utilisateur (progression, réglages persos, replays) DOIVENT primer sur les defaults du ZIP. Fix : mode « USER-DATA WINS » — on écrase toujours le fichier cible s'il existe. La version précédente contient soit la même valeur (no-op), soit la valeur customisée par l'opérateur (recherchée). Aucune raison légitime de préserver les defaults bundlés au détriment de user data. Logging bumpé Debug → Information sur les branches critiques de CopyPreviousSaveGamesAsync (nombre de versions scannées, path source résolu, count trouvé par sous-dossier). Facilite le diagnostic de ce genre de régression à l'avenir sans obliger l'opérateur à activer un mode verbose. Bump : 1.0.3 → 1.0.4 (bug fix). Co-Authored-By: Claude Opus 4.7 (1M context) --- installer/PSLauncher.iss | 2 +- src/PSLauncher.App/PSLauncher.App.csproj | 6 +- .../ViewModels/MainViewModel.cs | 72 ++++++++++++------- 3 files changed, 52 insertions(+), 28 deletions(-) diff --git a/installer/PSLauncher.iss b/installer/PSLauncher.iss index 3be871a..b9ec615 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.3" +#define MyAppVersion "1.0.4" #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 c05fe01..bec7227 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.3 - 1.0.3.0 - 1.0.3.0 + 1.0.4 + 1.0.4.0 + 1.0.4.0 true diff --git a/src/PSLauncher.App/ViewModels/MainViewModel.cs b/src/PSLauncher.App/ViewModels/MainViewModel.cs index f2364c4..09cfd49 100644 --- a/src/PSLauncher.App/ViewModels/MainViewModel.cs +++ b/src/PSLauncher.App/ViewModels/MainViewModel.cs @@ -2381,9 +2381,12 @@ public sealed partial class MainViewModel : ObservableObject /// (PROSERVE_UE_5_7.exePROSERVE_UE_5_7/). Compatible avec /// les passages d'une version UE à l'autre (UE_5_5 → UE_5_7) — on copie /// du dossier UE source vers le dossier UE cible (renommage transparent). - /// Mode COPIE NON DESTRUCTIVE : si le nouveau ZIP livre déjà un - /// fichier au même chemin (ex : profil par défaut bundlé), on NE - /// l'écrase PAS. Seuls les fichiers absents côté cible sont copiés. + /// Mode USER-DATA WINS : les .sav/.replay sont des + /// données utilisateur (progression, réglages, replays de session). Si + /// le nouveau ZIP livre un fichier au même chemin (ex : profil par + /// défaut bundlé GeneralSettings.sav), on l'ÉCRASE avec la + /// version de l'utilisateur — sinon les réglages custom de la version + /// précédente seraient perdus au profit des defaults du ZIP. /// Best-effort : exceptions IO loggées en warn mais non remontées — /// l'install ne doit pas échouer pour une copie de saves. /// @@ -2391,12 +2394,18 @@ public sealed partial class MainViewModel : ObservableObject private async Task CopyPreviousSaveGamesAsync(string newInstallDir, string newVersion, CancellationToken ct) { var installed = _registry.Scan(); + _logger.LogInformation( + "SaveGames copy for v{New} — scanning {Count} installed version(s): [{Versions}]", + newVersion, installed.Count, string.Join(", ", installed.Select(v => v.Version))); + var previous = installed .Where(v => !string.Equals(v.Version, newVersion, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(); if (previous is null) { - _logger.LogDebug("No previous version installed, nothing to copy for v{Version}", newVersion); + _logger.LogInformation( + "SaveGames copy skipped : no previous version found other than v{New} (aucune version antérieure à copier)", + newVersion); return; } @@ -2408,10 +2417,15 @@ public sealed partial class MainViewModel : ObservableObject var srcProjectName = Path.GetFileNameWithoutExtension(previous.ExecutablePath); if (string.IsNullOrWhiteSpace(srcProjectName)) { - _logger.LogDebug("Cannot resolve project name from previous exe {Exe}", previous.ExecutablePath); + _logger.LogWarning( + "SaveGames copy aborted : cannot resolve project name from previous exe {Exe}", + previous.ExecutablePath); return; } var srcSavedRoot = Path.Combine(previous.FolderPath, srcProjectName, "Saved"); + _logger.LogInformation( + "SaveGames copy source resolved : v{From} at {Root} (project = {Proj})", + previous.Version, srcSavedRoot, srcProjectName); // Pre-scan : on liste TOUS les fichiers à copier (savegames + demos) AVANT // de toucher quoi que ce soit, pour pouvoir afficher un count total dans @@ -2420,13 +2434,23 @@ public sealed partial class MainViewModel : ObservableObject foreach (var (subDir, glob) in PreservedSavedSubdirs) { var srcSubDir = Path.Combine(srcSavedRoot, subDir); - if (!Directory.Exists(srcSubDir)) continue; - foreach (var file in Directory.GetFiles(srcSubDir, glob, SearchOption.AllDirectories)) + if (!Directory.Exists(srcSubDir)) + { + _logger.LogInformation( + "SaveGames copy : source subdir absent, skipping — {Path}", srcSubDir); + continue; + } + var found = Directory.GetFiles(srcSubDir, glob, SearchOption.AllDirectories); + _logger.LogInformation( + "SaveGames copy : {Count} × {Glob} found in {Path}", found.Length, glob, srcSubDir); + foreach (var file in found) plan.Add((subDir, file)); } if (plan.Count == 0) { - _logger.LogDebug("Previous v{Version} has no save files / demos to copy", previous.Version); + _logger.LogInformation( + "SaveGames copy skipped : previous v{Version} has no matching files in {Root}", + previous.Version, srcSavedRoot); return; } @@ -2451,7 +2475,7 @@ public sealed partial class MainViewModel : ObservableObject "Copying {Count} user file(s) from v{From} ({SrcProj}) to v{To} ({DstProj}) — savegames+demos", plan.Count, previous.Version, srcProjectName, newVersion, dstProjectName); - int copied = 0, skippedExisting = 0; + int copied = 0, overwritten = 0; foreach (var (subDir, srcFile) in plan) { ct.ThrowIfCancellationRequested(); @@ -2460,20 +2484,20 @@ public sealed partial class MainViewModel : ObservableObject var dstFile = Path.Combine(dstSavedRoot, subDir, relPath); try { - if (File.Exists(dstFile)) - { - // Le ZIP livre déjà ce fichier (profil/replay bundlé) — on ne - // l'écrase pas. La version la plus à jour côté installer prime - // pour ce slot précis. Les autres fichiers (créés par l'opérateur - // en cours d'utilisation) sont copiés normalement. - skippedExisting++; - continue; - } + // Mode USER-DATA WINS : on ÉCRASE toujours le fichier cible s'il + // existe déjà. Le ZIP peut livrer des defaults (ex : + // GeneralSettings.sav avec les réglages usine) mais la version + // customisée par l'utilisateur dans l'install précédente prime — + // sinon les réglages custom seraient perdus à chaque upgrade au + // profit des defaults bundlés dans le ZIP. + bool wasOverwrite = File.Exists(dstFile); Directory.CreateDirectory(Path.GetDirectoryName(dstFile)!); - await using var src = new FileStream(srcFile, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, useAsync: true); - await using var dst = new FileStream(dstFile, FileMode.CreateNew, FileAccess.Write, FileShare.None, 81920, useAsync: true); - await src.CopyToAsync(dst, ct).ConfigureAwait(false); - copied++; + await using (var src = new FileStream(srcFile, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, useAsync: true)) + await using (var dst = new FileStream(dstFile, FileMode.Create, FileAccess.Write, FileShare.None, 81920, useAsync: true)) + { + await src.CopyToAsync(dst, ct).ConfigureAwait(false); + } + if (wasOverwrite) overwritten++; else copied++; } catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; } catch (Exception ex) @@ -2483,8 +2507,8 @@ public sealed partial class MainViewModel : ObservableObject } _logger.LogInformation( - "User data copy complete : {Copied} copied, {Skipped} skipped (already in new install)", - copied, skippedExisting); + "User data copy complete : {Copied} new + {Overwritten} overwritten (ZIP defaults replaced by user data)", + copied, overwritten); } private async Task UninstallVersionAsync(VersionRowViewModel row)