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.exe → PROSERVE_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)