v1.0.2 — Conserver les sauvegardes et replays entre versions
Nouvelle option dans le popup « Mise à jour disponible » (case cochée par défaut, décochable) : « Conserver les sauvegardes et replays de la version précédente ». Après extraction du ZIP de la nouvelle version, le launcher copie : • PROSERVE_UE_*/Saved/SaveGames/*.sav (profils + progression Unreal) • PROSERVE_UE_*/Saved/Demos/*.replay (replays de session pour debrief) Source = version installée au plus haut SemVer autre que celle qu'on vient d'installer (couvre upgrade ET ré-install). Le dossier projet UE est déduit du nom de l'exe (PROSERVE_UE_5_7.exe → PROSERVE_UE_5_7/) — supporte le passage UE_5_5 → UE_5_7 (renommage transparent : on copie du dossier source vers le dossier cible, peu importe leur numéro UE). Sémantique non-destructive : si la nouvelle install contient déjà un fichier au même chemin (profil/replay bundlé dans le ZIP), il n'est PAS écrasé. 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. Liste des sous-dossiers + globs en table statique (PreservedSavedSubdirs) pour faciliter l'ajout futur (Logs/Config user…). Best-effort : exceptions IO loggées en warn mais l'install n'échoue pas pour une copie qui foire (handle verrouillé, accès refusé). Si l'option est décochée OU si aucune version précédente n'est installée, no-op silencieux. Côté UI : checkbox au-dessus des boutons Plus tard / Télécharger, avec tooltip détaillant les deux chemins. État remonté via dialog.PreserveSaveGames et lu par MainViewModel APRÈS l'écriture du .proserve-meta.json (donc avant les redists & SteamVR merge). Pour les resumes de DL interrompus, le défaut est TRUE (l'utilisateur a déjà confirmé la première fois). i18n complète : FR/EN/CN/TH/AR/ES/DE pour le libellé de la case, le tooltip et le StatusMessage « Copie des sauvegardes et replays depuis vX.Y.Z (N fichiers)… ». Bump : 1.0.1 → 1.0.2 (feature patch). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
#define MyAppName "PROSERVE Launcher"
|
#define MyAppName "PROSERVE Launcher"
|
||||||
#define MyAppShortName "PS_Launcher"
|
#define MyAppShortName "PS_Launcher"
|
||||||
#define MyAppVersion "1.0.1"
|
#define MyAppVersion "1.0.2"
|
||||||
#define MyAppPublisher "ASTERION VR"
|
#define MyAppPublisher "ASTERION VR"
|
||||||
#define MyAppURL "https://asterionvr.com"
|
#define MyAppURL "https://asterionvr.com"
|
||||||
#define MyAppExeName "PS_Launcher.exe"
|
#define MyAppExeName "PS_Launcher.exe"
|
||||||
|
|||||||
@@ -18,9 +18,9 @@
|
|||||||
<Product>PROSERVE Launcher</Product>
|
<Product>PROSERVE Launcher</Product>
|
||||||
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
<Copyright>© 2026 ASTERION VR — All rights reserved</Copyright>
|
||||||
<RootNamespace>PSLauncher.App</RootNamespace>
|
<RootNamespace>PSLauncher.App</RootNamespace>
|
||||||
<Version>1.0.1</Version>
|
<Version>1.0.2</Version>
|
||||||
<AssemblyVersion>1.0.1.0</AssemblyVersion>
|
<AssemblyVersion>1.0.2.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.1.0</FileVersion>
|
<FileVersion>1.0.2.0</FileVersion>
|
||||||
|
|
||||||
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
<!-- Single-file self-contained publish profile (used by `dotnet publish`) -->
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
|
|||||||
@@ -1477,6 +1477,12 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
// l'utilisateur a déjà confirmé sa décision la première fois.
|
// l'utilisateur a déjà confirmé sa décision la première fois.
|
||||||
var isResume = row.HasResumableDownload;
|
var isResume = row.HasResumableDownload;
|
||||||
|
|
||||||
|
// Préservation des savegames : par défaut TRUE (option cochée dans la
|
||||||
|
// popup). Si l'install est un resume, l'utilisateur a déjà confirmé sa
|
||||||
|
// décision la première fois — on garde le défaut (préserver) plutôt que
|
||||||
|
// de re-bother avec un dialog.
|
||||||
|
bool preserveSaveGames = true;
|
||||||
|
|
||||||
if (!isResume)
|
if (!isResume)
|
||||||
{
|
{
|
||||||
// 1) Récupère release notes (best effort).
|
// 1) Récupère release notes (best effort).
|
||||||
@@ -1503,6 +1509,7 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
var dialog = new UpdateAvailableDialog(row.Remote, notes) { Owner = Application.Current.MainWindow };
|
var dialog = new UpdateAvailableDialog(row.Remote, notes) { Owner = Application.Current.MainWindow };
|
||||||
dialog.ShowDialog();
|
dialog.ShowDialog();
|
||||||
if (!dialog.DownloadRequested) return;
|
if (!dialog.DownloadRequested) return;
|
||||||
|
preserveSaveGames = dialog.PreserveSaveGames;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Download — résolution d'URL en deux temps :
|
// 3) Download — résolution d'URL en deux temps :
|
||||||
@@ -1661,6 +1668,31 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
_logger.LogWarning(ex, "Failed to write install metadata (will fall back to glob detection)");
|
_logger.LogWarning(ex, "Failed to write install metadata (will fall back to glob detection)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Préservation des savegames — copie les .sav (PROSERVE_UE_*/Saved/SaveGames/*)
|
||||||
|
// depuis la version installée la plus récente AUTRE que celle qu'on vient
|
||||||
|
// d'installer vers le nouveau dossier d'install. Best-effort : si la
|
||||||
|
// copie échoue (handle verrouillé, accès refusé), on log un warn mais on
|
||||||
|
// ne bloque pas l'install — l'utilisateur peut copier manuellement.
|
||||||
|
// L'option est cochée par défaut dans le dialog mais l'utilisateur peut
|
||||||
|
// la décocher (cas : premier install propre, ou release qui invalide
|
||||||
|
// explicitement les anciennes saves).
|
||||||
|
if (preserveSaveGames)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await CopyPreviousSaveGamesAsync(target, row.Version, ct);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Save games copy threw an unhandled exception (continuing install)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Save games preservation disabled by user for v{Version}", row.Version);
|
||||||
|
}
|
||||||
|
|
||||||
// Installation des redists Unreal (VC_redist, UEPrereqSetup, etc.) si la
|
// Installation des redists Unreal (VC_redist, UEPrereqSetup, etc.) si la
|
||||||
// version contient un dossier `_redist/`. Run silent + UAC élévation par
|
// version contient un dossier `_redist/`. Run silent + UAC élévation par
|
||||||
// installer. Track via metadata pour ne pas re-runner à chaque réinstall.
|
// installer. Track via metadata pour ne pas re-runner à chaque réinstall.
|
||||||
@@ -2307,6 +2339,154 @@ public sealed partial class MainViewModel : ObservableObject
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Liste des sous-dossiers <c>PROSERVE_UE_*/Saved/{sub}/</c> à préserver
|
||||||
|
/// entre versions, avec le glob filtre par sous-dossier. La copie est
|
||||||
|
/// récursive (<c>AllDirectories</c>) et non destructive (les fichiers
|
||||||
|
/// déjà présents dans la nouvelle install ne sont jamais écrasés).
|
||||||
|
///
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><b>SaveGames / *.sav</b> — profils Unreal, progression
|
||||||
|
/// d'entraînement, données opérateur.</item>
|
||||||
|
/// <item><b>Demos / *.replay</b> — replays Unreal (enregistrements de
|
||||||
|
/// session, extension <c>.replay</c> dans PROSERVE). Précieux pour le
|
||||||
|
/// debriefing post-formation.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
private static readonly (string SubDir, string Glob)[] PreservedSavedSubdirs =
|
||||||
|
{
|
||||||
|
("SaveGames", "*.sav"),
|
||||||
|
("Demos", "*.replay"),
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Copie les fichiers utilisateur (.sav + .demo) depuis les sous-dossiers
|
||||||
|
/// <c>PROSERVE_UE_*/Saved/SaveGames/</c> et <c>PROSERVE_UE_*/Saved/Demos/</c>
|
||||||
|
/// de la version installée la plus récente (autre que celle qu'on vient
|
||||||
|
/// d'installer) vers le même chemin relatif dans le nouveau dossier d'install.
|
||||||
|
///
|
||||||
|
/// Cas d'usage : l'opérateur installe v1.5.5 par-dessus v1.5.4 — sans ça, les
|
||||||
|
/// .sav Unreal (progression d'entraînement, profils) et les .demo (replays
|
||||||
|
/// de session) de v1.5.4 ne seraient pas visibles depuis v1.5.5 car ces
|
||||||
|
/// fichiers vivent dans le dossier d'install du jeu, pas dans %LocalAppData%.
|
||||||
|
/// Activé par défaut (case cochée dans <see cref="UpdateAvailableDialog"/>),
|
||||||
|
/// décochable.
|
||||||
|
///
|
||||||
|
/// Détails :
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>Source = version installée au plus haut SemVer, autre que
|
||||||
|
/// <paramref name="newVersion"/>. Si l'opérateur fait une réinstall ou un
|
||||||
|
/// downgrade, on copie quand même depuis la plus récente DISPONIBLE.</item>
|
||||||
|
/// <item>Dossier projet UE détecté via le nom de l'exe
|
||||||
|
/// (<c>PROSERVE_UE_5_7.exe</c> → <c>PROSERVE_UE_5_7/</c>). 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).</item>
|
||||||
|
/// <item>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.</item>
|
||||||
|
/// <item>Best-effort : exceptions IO loggées en warn mais non remontées —
|
||||||
|
/// l'install ne doit pas échouer pour une copie de saves.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
private async Task CopyPreviousSaveGamesAsync(string newInstallDir, string newVersion, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var installed = _registry.Scan();
|
||||||
|
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);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dossier projet UE = nom de l'exe sans extension. L'exe live à la racine
|
||||||
|
// de l'install, le projet UE dans un sous-dossier homonyme :
|
||||||
|
// <install>/PROSERVE_UE_5_7.exe
|
||||||
|
// <install>/PROSERVE_UE_5_7/Saved/SaveGames/*.sav
|
||||||
|
// <install>/PROSERVE_UE_5_7/Saved/Demos/*.replay
|
||||||
|
var srcProjectName = Path.GetFileNameWithoutExtension(previous.ExecutablePath);
|
||||||
|
if (string.IsNullOrWhiteSpace(srcProjectName))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Cannot resolve project name from previous exe {Exe}", previous.ExecutablePath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var srcSavedRoot = Path.Combine(previous.FolderPath, srcProjectName, "Saved");
|
||||||
|
|
||||||
|
// Pre-scan : on liste TOUS les fichiers à copier (savegames + demos) AVANT
|
||||||
|
// de toucher quoi que ce soit, pour pouvoir afficher un count total dans
|
||||||
|
// le StatusMessage et early-return si vraiment rien à faire.
|
||||||
|
var plan = new List<(string SubDir, string SrcFile)>();
|
||||||
|
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))
|
||||||
|
plan.Add((subDir, file));
|
||||||
|
}
|
||||||
|
if (plan.Count == 0)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Previous v{Version} has no save files / demos to copy", previous.Version);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Côté cible, on résout le NOUVEAU dossier UE via la même règle : lit la
|
||||||
|
// metadata fraîchement écrite (qui contient l'exe du manifest), tombe sur
|
||||||
|
// glob PROSERVE_UE_*.exe sinon. Le dossier projet peut différer du source
|
||||||
|
// (downgrade UE, ou bump UE_5_5 → UE_5_7).
|
||||||
|
var newExe = Directory.GetFiles(newInstallDir, "PROSERVE_UE_*.exe", SearchOption.TopDirectoryOnly)
|
||||||
|
.OrderBy(p => p, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (newExe is null)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Cannot find PROSERVE_UE_*.exe in {Dir} — skipping save games copy", newInstallDir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dstProjectName = Path.GetFileNameWithoutExtension(newExe);
|
||||||
|
var dstSavedRoot = Path.Combine(newInstallDir, dstProjectName, "Saved");
|
||||||
|
|
||||||
|
StatusMessage = Strings.StatusCopyingSaveGames(previous.Version, plan.Count);
|
||||||
|
ProgressDetail = null;
|
||||||
|
_logger.LogInformation(
|
||||||
|
"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;
|
||||||
|
foreach (var (subDir, srcFile) in plan)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
var srcSubDir = Path.Combine(srcSavedRoot, subDir);
|
||||||
|
var relPath = Path.GetRelativePath(srcSubDir, srcFile);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(ex, "Failed to copy user file {Src} → {Dst}", srcFile, dstFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogInformation(
|
||||||
|
"User data copy complete : {Copied} copied, {Skipped} skipped (already in new install)",
|
||||||
|
copied, skippedExisting);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task UninstallVersionAsync(VersionRowViewModel row)
|
private async Task UninstallVersionAsync(VersionRowViewModel row)
|
||||||
{
|
{
|
||||||
if (row.Installed is null) return;
|
if (row.Installed is null) return;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Grid.Row="0"
|
<TextBlock Grid.Row="0"
|
||||||
@@ -37,7 +38,16 @@
|
|||||||
Background="Transparent" />
|
Background="Transparent" />
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,18,0,0">
|
<CheckBox Grid.Row="3"
|
||||||
|
x:Name="PreserveSaveGamesCheckBox"
|
||||||
|
Content="{x:Static loc:Strings.UpdatePreserveSaveGames}"
|
||||||
|
ToolTip="{x:Static loc:Strings.UpdatePreserveSaveGamesTooltip}"
|
||||||
|
IsChecked="True"
|
||||||
|
Foreground="{StaticResource Brush.Text.Primary}"
|
||||||
|
Margin="0,14,0,0"
|
||||||
|
HorizontalAlignment="Left" />
|
||||||
|
|
||||||
|
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,14,0,0">
|
||||||
<Button Style="{StaticResource SecondaryButton}"
|
<Button Style="{StaticResource SecondaryButton}"
|
||||||
Content="{x:Static loc:Strings.ActionLater}"
|
Content="{x:Static loc:Strings.ActionLater}"
|
||||||
IsCancel="True"
|
IsCancel="True"
|
||||||
|
|||||||
@@ -24,9 +24,18 @@ public partial class UpdateAvailableDialog : Window
|
|||||||
|
|
||||||
public bool DownloadRequested { get; private set; }
|
public bool DownloadRequested { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// État de la case « Conserver les sauvegardes de la version précédente ».
|
||||||
|
/// Cochée par défaut, lue par MainViewModel APRÈS l'extraction du ZIP pour
|
||||||
|
/// décider s'il faut copier les .sav depuis l'install précédente vers la
|
||||||
|
/// nouvelle (voir CopyPreviousSaveGamesAsync).
|
||||||
|
/// </summary>
|
||||||
|
public bool PreserveSaveGames { get; private set; } = true;
|
||||||
|
|
||||||
private void OnDownload(object sender, RoutedEventArgs e)
|
private void OnDownload(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
DownloadRequested = true;
|
DownloadRequested = true;
|
||||||
|
PreserveSaveGames = PreserveSaveGamesCheckBox.IsChecked == true;
|
||||||
DialogResult = true;
|
DialogResult = true;
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -278,6 +278,24 @@ public static class Strings
|
|||||||
// ==================== UPDATE ====================
|
// ==================== UPDATE ====================
|
||||||
public static string UpdateAvailableTitle => T("Mise à jour disponible", "Update available", "有可用更新", "มีการอัปเดต", "تحديث متاح");
|
public static string UpdateAvailableTitle => T("Mise à jour disponible", "Update available", "有可用更新", "มีการอัปเดต", "تحديث متاح");
|
||||||
public static string UpdateDownload => T("⬇ Télécharger", "⬇ Download", "⬇ 下载", "⬇ ดาวน์โหลด", "⬇ تنزيل");
|
public static string UpdateDownload => T("⬇ Télécharger", "⬇ Download", "⬇ 下载", "⬇ ดาวน์โหลด", "⬇ تنزيل");
|
||||||
|
public static string UpdatePreserveSaveGames => T(
|
||||||
|
"Conserver les sauvegardes et replays de la version précédente",
|
||||||
|
"Keep save games and replays from the previous version",
|
||||||
|
"保留上一版本的存档和录像",
|
||||||
|
"เก็บไฟล์เซฟและรีเพลย์จากเวอร์ชันก่อนหน้า",
|
||||||
|
"الاحتفاظ بحفظات اللعبة والإعادات من النسخة السابقة",
|
||||||
|
"Conservar las partidas guardadas y repeticiones de la versión anterior",
|
||||||
|
"Spielstände und Replays der vorherigen Version beibehalten"
|
||||||
|
);
|
||||||
|
public static string UpdatePreserveSaveGamesTooltip => T(
|
||||||
|
"Copie les fichiers .sav (PROSERVE_UE_*/Saved/SaveGames) ET les replays .replay (PROSERVE_UE_*/Saved/Demos) de la version la plus récente déjà installée vers la nouvelle installation, après extraction. Si aucune version précédente n'est trouvée, l'option est sans effet.",
|
||||||
|
"Copies .sav files (PROSERVE_UE_*/Saved/SaveGames) AND .replay files (PROSERVE_UE_*/Saved/Demos) from the most recent already-installed version into the new install, after extraction. If no previous version is found, the option has no effect.",
|
||||||
|
"在解压后将最近已安装版本的 .sav 存档 (PROSERVE_UE_*/Saved/SaveGames) 和 .replay 录像 (PROSERVE_UE_*/Saved/Demos) 复制到新安装中。如果未找到之前的版本,则此选项无效。",
|
||||||
|
"คัดลอกไฟล์ .sav (PROSERVE_UE_*/Saved/SaveGames) และไฟล์ .replay (PROSERVE_UE_*/Saved/Demos) จากเวอร์ชันที่ติดตั้งล่าสุดไปยังการติดตั้งใหม่ หลังจากการแตกไฟล์ หากไม่พบเวอร์ชันก่อนหน้า ตัวเลือกนี้จะไม่มีผล",
|
||||||
|
"ينسخ ملفات .sav (PROSERVE_UE_*/Saved/SaveGames) وملفات .replay (PROSERVE_UE_*/Saved/Demos) من أحدث نسخة مثبتة بالفعل إلى التثبيت الجديد، بعد الاستخراج. إذا لم يتم العثور على نسخة سابقة، فلن يكون لهذا الخيار أي تأثير.",
|
||||||
|
"Copia los archivos .sav (PROSERVE_UE_*/Saved/SaveGames) y los archivos .replay (PROSERVE_UE_*/Saved/Demos) de la versión más reciente ya instalada en la nueva instalación, tras la extracción. Si no se encuentra una versión anterior, la opción no surte efecto.",
|
||||||
|
"Kopiert die .sav-Dateien (PROSERVE_UE_*/Saved/SaveGames) UND .replay-Dateien (PROSERVE_UE_*/Saved/Demos) der zuletzt installierten Version nach dem Entpacken in die neue Installation. Wird keine vorherige Version gefunden, hat die Option keine Wirkung."
|
||||||
|
);
|
||||||
public static string LauncherUpdateTitle => T("Mise à jour du launcher", "Launcher update", "启动器更新", "อัปเดต Launcher", "تحديث المُشغِّل");
|
public static string LauncherUpdateTitle => T("Mise à jour du launcher", "Launcher update", "启动器更新", "อัปเดต Launcher", "تحديث المُشغِّل");
|
||||||
public static string LauncherUpdateAvailable => T("Mise à jour du launcher disponible", "Launcher update available", "启动器更新可用", "มีการอัปเดต Launcher", "تحديث المُشغِّل متاح");
|
public static string LauncherUpdateAvailable => T("Mise à jour du launcher disponible", "Launcher update available", "启动器更新可用", "มีการอัปเดต Launcher", "تحديث المُشغِّل متاح");
|
||||||
public static string LauncherUpdateBody => T(
|
public static string LauncherUpdateBody => T(
|
||||||
@@ -565,6 +583,16 @@ public static class Strings
|
|||||||
$"تم تثبيت v{version} بنجاح"
|
$"تم تثبيت v{version} بنجاح"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
public static string StatusCopyingSaveGames(string fromVersion, int fileCount) => T(
|
||||||
|
$"Copie des sauvegardes et replays depuis v{fromVersion} ({fileCount} fichier{(fileCount > 1 ? "s" : "")})…",
|
||||||
|
$"Copying save games and replays from v{fromVersion} ({fileCount} file{(fileCount > 1 ? "s" : "")})…",
|
||||||
|
$"正在从 v{fromVersion} 复制存档和录像({fileCount} 个文件)…",
|
||||||
|
$"กำลังคัดลอกไฟล์เซฟและรีเพลย์จาก v{fromVersion} ({fileCount} ไฟล์)…",
|
||||||
|
$"جارٍ نسخ حفظات اللعبة والإعادات من v{fromVersion} ({fileCount} ملف)…",
|
||||||
|
$"Copiando partidas guardadas y repeticiones desde v{fromVersion} ({fileCount} archivo{(fileCount > 1 ? "s" : "")})…",
|
||||||
|
$"Spielstände und Replays werden von v{fromVersion} kopiert ({fileCount} Datei{(fileCount > 1 ? "en" : "")})…"
|
||||||
|
);
|
||||||
|
|
||||||
public static string StatusUninstallingVersion(string version) => T(
|
public static string StatusUninstallingVersion(string version) => T(
|
||||||
$"Suppression v{version}…",
|
$"Suppression v{version}…",
|
||||||
$"Uninstalling v{version}…",
|
$"Uninstalling v{version}…",
|
||||||
|
|||||||
Reference in New Issue
Block a user