Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions PCL.Core/App/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,11 @@ partial class HideConfigGroup
/// </summary>
[ConfigItem<int>("LaunchArgumentIndieV2", 4, ConfigSource.Local)] public partial int IndieSolutionV2 { get; set; }

/// <summary>
/// 删除隔离实例前的个人文件备份模式。
/// </summary>
[ConfigItem<PersonalFilesBackupMode>("LaunchArgumentPersonalFilesBackupMode", PersonalFilesBackupMode.AskEveryTime, ConfigSource.Local)] public partial PersonalFilesBackupMode PersonalFilesBackup { get; set; }

/// <summary>
/// 游戏启动后启动器可见性。
/// </summary>
Expand Down
10 changes: 10 additions & 0 deletions PCL.Core/App/ConfigEnums.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,16 @@ public enum GameProcessPriority
RealTime = 4
}

/// <summary>
/// 删除实例时的个人文件备份模式。
/// </summary>
public enum PersonalFilesBackupMode
{
Disabled = 0,
AskEveryTime = 1,
Always = 2
}

/// <summary>
/// 游戏启动后启动器可见性
/// </summary>
Expand Down
14 changes: 13 additions & 1 deletion PCL.Core/App/Localization/Languages/en-US.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,14 @@
<sys:String x:Key="Instance.Overall.Shortcuts.SavesFolder">Saves folder</sys:String>
<sys:String x:Key="Instance.Overall.Shortcuts.Title">Shortcuts</sys:String>
<sys:String x:Key="Instance.Overall.Unfavorite">Unfavorite</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Ask.BackupAndDelete">Back up and delete</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Ask.DeleteWithoutBackup">Delete without backup</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Ask.Message">Screenshots were found in instance {0}. Back them up before deletion?&#xA;Schematics will be backed up at the same time.</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Ask.Title">Back up screenshots</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.AskHint">When screenshots are found, you will be asked whether to back them up before deletion.</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.DeleteHint">Files will be backed up to a folder before deletion.</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Failed">Failed to back up screenshots or schematics. The instance was kept. See the error details.</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Success">Backed up {0} personal file(s) to: {1}</sys:String>

<!-- Instance.Overall.Icon -->

Expand Down Expand Up @@ -2817,6 +2825,10 @@
<sys:String x:Key="Setup.Launch.Options.InstanceIsolation.NonRelease.ToolTip">Isolate snapshots, pre-releases, old versions, and April Fools' versions from other instances.</sys:String>
<sys:String x:Key="Setup.Launch.Options.InstanceIsolation.None.ToolTip">All instances use the same profile. Saves, mods, resource packs, and other files are shared.&#xA;If multiple instances with mods installed are present, this may cause mod conflicts.</sys:String>
<sys:String x:Key="Setup.Launch.Options.InstanceIsolation.ToolTip">When installing a new instance, the isolation option for the new instance will be automatically set accordingly.&#xA;To adjust the isolation policy for an existing instance, please go to instance settings.</sys:String>
<sys:String x:Key="Setup.Launch.Options.PersonalFilesBackup">Protect screenshots when deleting isolated instances</sys:String>
<sys:String x:Key="Setup.Launch.Options.PersonalFilesBackup.Always">Always back up</sys:String>
<sys:String x:Key="Setup.Launch.Options.PersonalFilesBackup.AskEveryTime">Ask every time</sys:String>
<sys:String x:Key="Setup.Launch.Options.PersonalFilesBackup.ToolTip">Backups are saved to PCL\PersonalFiles in the current Minecraft folder. Schematics are included when screenshots are backed up. Deletion is canceled if the backup fails.</sys:String>

<!-- Setup.Launch.Options.Visibility -->

Expand Down Expand Up @@ -3570,4 +3582,4 @@
<sys:String x:Key="Watcher.WindowLoaded">Minecraft window loaded: {0} ({1})</sys:String>
<sys:String x:Key="Watcher.WindowMaximized">Minecraft window maximized: {0}</sys:String>

</ResourceDictionary>
</ResourceDictionary>
14 changes: 13 additions & 1 deletion PCL.Core/App/Localization/Languages/zh-CN.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,14 @@
<sys:String x:Key="Instance.Overall.Shortcuts.SavesFolder">存档文件夹</sys:String>
<sys:String x:Key="Instance.Overall.Shortcuts.Title">快捷方式</sys:String>
<sys:String x:Key="Instance.Overall.Unfavorite">从收藏夹中移除</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Ask.BackupAndDelete">备份并删除</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Ask.DeleteWithoutBackup">直接删除</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Ask.Message">检测到实例 {0} 中有截图,是否在删除前备份?&#xA;投影原理图也会一并备份。</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Ask.Title">备份截图</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.AskHint">检测到截图时,删除前会询问是否备份。</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.DeleteHint">删除前会备份文件到文件夹。</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Failed">备份截图或投影原理图失败,实例未删除。请查看错误详情。</sys:String>
<sys:String x:Key="Instance.PersonalFiles.Backup.Success">已将 {0} 个回忆文件备份到:{1}</sys:String>

<!-- Instance.Overall.Icon -->

Expand Down Expand Up @@ -2817,6 +2825,10 @@
<sys:String x:Key="Setup.Launch.Options.InstanceIsolation.NonRelease.ToolTip">将 Minecraft 快照、预发布版、远古版本、愚人节版本与其他实例进行隔离</sys:String>
<sys:String x:Key="Setup.Launch.Options.InstanceIsolation.None.ToolTip">所有实例均使用同一档案,存档、模组、资源包等均为公用。&#xA;若存在多个安装了模组的实例,可能会导致模组冲突。</sys:String>
<sys:String x:Key="Setup.Launch.Options.InstanceIsolation.ToolTip">当安装新实例时,据此自动设置新实例的隔离选项。&#xA;若想调整已有实例的隔离策略,请前往它的实例设置。</sys:String>
<sys:String x:Key="Setup.Launch.Options.PersonalFilesBackup">删除隔离实例时保护截图</sys:String>
<sys:String x:Key="Setup.Launch.Options.PersonalFilesBackup.Always">始终备份</sys:String>
<sys:String x:Key="Setup.Launch.Options.PersonalFilesBackup.AskEveryTime">每次询问</sys:String>
<sys:String x:Key="Setup.Launch.Options.PersonalFilesBackup.ToolTip">备份文件会保存到当前 Minecraft 文件夹下的 PCL\PersonalFiles。备份截图时也会保存投影原理图;备份失败时会取消删除。</sys:String>

<!-- Setup.Launch.Options.Visibility -->

Expand Down Expand Up @@ -3570,4 +3582,4 @@
<sys:String x:Key="Watcher.WindowLoaded">Minecraft 窗口已加载:{0}({1})</sys:String>
<sys:String x:Key="Watcher.WindowMaximized">已最大化 Minecraft 窗口:{0}</sys:String>

</ResourceDictionary>
</ResourceDictionary>
106 changes: 106 additions & 0 deletions Plain Craft Launcher 2/Modules/Minecraft/ModPersonalFiles.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System.IO;
using PCL.Core.App;
using PCL.Core.App.Localization;
using PCL.Core.UI;

namespace PCL;

public static class ModPersonalFiles
{
private static string _ArchiveRoot =>
Path.GetFullPath(Path.Combine(ModFolder.mcFolderSelected, "PCL", "PersonalFiles"));

private static (int FileCount, string TargetFolder) _Backup(McInstance instance)
{
var targetFolder = Path.Combine(_ArchiveRoot, instance.Name);
var instancePath = Path.GetFullPath(instance.PathInstance);
if (!instancePath.EndsWith(Path.DirectorySeparatorChar)) instancePath += Path.DirectorySeparatorChar;
if (targetFolder.StartsWith(instancePath, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("回忆文件备份目录不能位于实例目录中");

var fileCount = _CopyDirectory(Path.Combine(instance.PathIndie, "screenshots"),
Path.Combine(targetFolder, "screenshots"));
fileCount += _CopyDirectory(Path.Combine(instance.PathIndie, "schematics"),
Path.Combine(targetFolder, "schematics"));

if (fileCount > 0)
ModBase.Log($"[PersonalFiles] 已备份实例 {instance.Name} 的 {fileCount} 个回忆文件到 {targetFolder}");
return (fileCount, targetFolder);
}

public static string? GetDeleteHint()
{
return Config.Launch.PersonalFilesBackup switch
{
PersonalFilesBackupMode.Disabled => null,
PersonalFilesBackupMode.AskEveryTime => Lang.Text("Instance.PersonalFiles.Backup.AskHint"),
_ => Lang.Text("Instance.PersonalFiles.Backup.DeleteHint")
};
}

public static bool TryHandleBeforeDelete(McInstance instance)
{
try
{
var backupMode = Config.Launch.PersonalFilesBackup;
if (backupMode == PersonalFilesBackupMode.Disabled) return true;
if (backupMode == PersonalFilesBackupMode.AskEveryTime)
{
if (!_HasScreenshots(instance.PathIndie)) return true;
var promptResult = ModMain.MyMsgBox(
Lang.Text("Instance.PersonalFiles.Backup.Ask.Message", instance.Name),
Lang.Text("Instance.PersonalFiles.Backup.Ask.Title"),
Lang.Text("Instance.PersonalFiles.Backup.Ask.BackupAndDelete"),
Lang.Text("Instance.PersonalFiles.Backup.Ask.DeleteWithoutBackup"),
Lang.Text("Common.Action.Cancel"));
if (promptResult == 2) return true;
if (promptResult != 1) return false;
}

var result = _Backup(instance);
if (result.FileCount > 0)
HintService.Hint(Lang.Text("Instance.PersonalFiles.Backup.Success", result.FileCount,
result.TargetFolder), HintType.Success);
return true;
}
catch (Exception ex)
{
ModBase.Log(ex, $"备份实例 {instance.Name} 的回忆文件失败", ModBase.LogLevel.Msgbox,
userSummary: Lang.Text("Instance.PersonalFiles.Backup.Failed"));
return false;
}
}

private static bool _HasScreenshots(string instanceFolder)
{
var screenshotFolder = Path.Combine(instanceFolder, "screenshots");
return Directory.Exists(screenshotFolder) &&
Directory.EnumerateFiles(screenshotFolder, "*", _CreateEnumerationOptions()).Any();
}

private static int _CopyDirectory(string sourceFolder, string targetFolder)
{
if (!Directory.Exists(sourceFolder)) return 0;
var fileCount = 0;
foreach (var sourceFile in Directory.EnumerateFiles(sourceFolder, "*", _CreateEnumerationOptions()))
{
var targetFile = Path.Combine(targetFolder, Path.GetRelativePath(sourceFolder, sourceFile));
ModBase.CopyFile(sourceFile, targetFile);
File.SetCreationTimeUtc(targetFile, File.GetCreationTimeUtc(sourceFile));
File.SetLastWriteTimeUtc(targetFile, File.GetLastWriteTimeUtc(sourceFile));
fileCount += 1;
}

return fileCount;
}

private static EnumerationOptions _CreateEnumerationOptions()
{
return new EnumerationOptions
{
RecurseSubdirectories = true,
IgnoreInaccessible = false,
AttributesToSkip = FileAttributes.ReparsePoint
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -751,9 +751,12 @@ private void BtnManageDelete_Click(object sender, MouseButtonEventArgs e)
(false, true) => "Instance.Overall.Delete.ConfirmMessagePermanent",
(false, false) => "Instance.Overall.Delete.ConfirmMessage"
};
var confirmMessage = Lang.Text(confirmMessageKey, PageInstanceLeft.McInstance.Name);
var backupHint = isIsolatedInstance ? ModPersonalFiles.GetDeleteHint() : null;
if (backupHint is not null) confirmMessage += "\r\n" + backupHint;

var confirmResult = ModMain.MyMsgBox(
Lang.Text(confirmMessageKey, PageInstanceLeft.McInstance.Name),
confirmMessage,
Lang.Text("Instance.Overall.Delete.ConfirmTitle"),
button2: Lang.Text("Common.Action.Cancel"),
isWarn: isIsolatedInstance || isShiftPressed
Expand All @@ -763,6 +766,8 @@ private void BtnManageDelete_Click(object sender, MouseButtonEventArgs e)
{
case 1:
{
if (isIsolatedInstance &&
!ModPersonalFiles.TryHandleBeforeDelete(PageInstanceLeft.McInstance)) return;
var instancePath = PageInstanceLeft.McInstance.PathInstance;
var instanceName = PageInstanceLeft.McInstance.Name;
ModBase.IniClearCache(Path.Combine(PageInstanceLeft.McInstance.PathIndie, "options.txt"));
Expand Down
8 changes: 6 additions & 2 deletions Plain Craft Launcher 2/Pages/PageSelectRight.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -532,17 +532,21 @@ public static void DeleteVersion(MyListItem item, McInstance mcInstance)
{
var isShiftPressed = Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift);
var isHintIndie = mcInstance.state != McInstanceState.Error &&
(mcInstance.PathIndie ?? "") != (ModFolder.mcFolderSelected ?? "");
!string.Equals(mcInstance.PathIndie, ModFolder.mcFolderSelected,
StringComparison.OrdinalIgnoreCase);
var confirmMsg = isShiftPressed
? Lang.Text("Select.Instance.Delete.ConfirmPermanentMessage", mcInstance.Name)
: Lang.Text("Select.Instance.Delete.ConfirmMessage", mcInstance.Name);
var confirmFullMsg = confirmMsg +
(isHintIndie ? "\r\n" + Lang.Text("Select.Instance.Delete.IsolatedWarning") : "");
(isHintIndie ? "\r\n" + Lang.Text("Select.Instance.Delete.IsolatedWarning") : "");
var backupHint = isHintIndie ? ModPersonalFiles.GetDeleteHint() : null;
if (backupHint is not null) confirmFullMsg += "\r\n" + backupHint;
switch (ModMain.MyMsgBox(confirmFullMsg, Lang.Text("Select.Instance.Delete.ConfirmTitle"),
button2: Lang.Text("Common.Action.Cancel"), isWarn: true))
{
case 1:
{
if (isHintIndie && !ModPersonalFiles.TryHandleBeforeDelete(mcInstance)) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Back up isolated instances even when validation fails

When an isolated instance has a corrupt or unreadable version definition, its state is McInstanceState.Error, which forces isHintIndie to false and skips this new backup handler even if PathIndie is the instance directory. The delete operation still removes PathInstance, so screenshots and schematics are silently lost under both Ask and Always modes; determine isolation from the configured/path relationship or probe the instance directory independently of validation state.

Useful? React with 👍 / 👎.

ModBase.IniClearCache(Path.Combine(mcInstance.PathIndie, "options.txt"));
((DynamicCacheConfigStorage)ConfigService.GetProvider(ConfigSource.GameInstance)).InvalidateCache(
mcInstance.PathInstance);
Expand Down
19 changes: 19 additions & 0 deletions Plain Craft Launcher 2/Pages/PageSetup/PageSetupLaunch.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,25 @@
<local:MyComboBoxItem Content="{DynamicResource Setup.Launch.Options.IpStack.Ipv6}" />
</local:MyComboBox>
</Grid>
<Grid Margin="0,10,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="Name" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="60" />
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" HorizontalAlignment="Left"
Text="{DynamicResource Setup.Launch.Options.PersonalFilesBackup}"
Margin="0,0,25,0" />
<local:MyComboBox x:Name="ComboArgumentPersonalFilesBackup" Grid.Column="1"
Grid.ColumnSpan="2" Tag="LaunchArgumentPersonalFilesBackupMode"
SelectionChanged="ComboChange"
ToolTip="{DynamicResource Setup.Launch.Options.PersonalFilesBackup.ToolTip}">
<local:MyComboBoxItem Content="{DynamicResource Common.Action.Close}" />
<local:MyComboBoxItem Content="{DynamicResource Setup.Launch.Options.PersonalFilesBackup.AskEveryTime}"
IsSelected="True" />
<local:MyComboBoxItem Content="{DynamicResource Setup.Launch.Options.PersonalFilesBackup.Always}" />
</local:MyComboBox>
</Grid>
</StackPanel>
</local:MyCard>
<local:MyCard Margin="0,0,0,15" Title="{DynamicResource Setup.Launch.Memory.Title}">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ public void Reload()
TextArgumentTitle.Text = Config.Launch.Title;
TextArgumentInfo.Text = Config.Launch.TypeInfo;
ComboArgumentIndieV2.SelectedIndex = Config.Launch.IndieSolutionV2;
ComboArgumentPersonalFilesBackup.SelectedIndex = (int)Config.Launch.PersonalFilesBackup;
ComboArgumentVisibie.SelectedIndex = (int)Config.Launch.LauncherVisibility;
ComboArgumentPriority.SelectedValue = ((int)Config.Launch.ProcessPriority).ToString();
ComboArgumentWindowType.SelectedIndex = (int)Config.Launch.GameWindowMode;
Expand Down
Loading