From 001e75c90adb80d7d36ea230eabb9d67749861cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=83=9D=E6=9F=90=E4=BA=BABH?= <1218271192@qq.com> Date: Tue, 4 Aug 2026 13:08:28 +0800 Subject: [PATCH] =?UTF-8?q?Fix:=20=E5=88=A0=E9=99=A4=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E8=BF=87=E7=A8=8B=E4=B8=AD=E4=BF=AE=E6=94=B9=E5=AE=9E=E4=BE=8B?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E5=AF=BC=E8=87=B4=E5=AE=9E=E4=BE=8B=E7=9B=AE?= =?UTF-8?q?=E5=BD=95=E6=AE=8B=E7=95=99=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hmcl/game/HMCLGameRepository.java | 51 +++++++++++++++---- .../hmcl/ui/instances/Instances.java | 5 ++ 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index 0b704310602..396218d2c41 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -67,6 +67,7 @@ import java.nio.file.Path; import java.time.Instant; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -109,6 +110,8 @@ public record InstanceReference(HMCLGameRepository repository, @Nullable GameIns private final Set loadedInstanceGameSettings = new HashSet<>(); private final Set readOnlyInstanceGameSettings = new HashSet<>(); private final Set beingModpackInstances = new HashSet<>(); + /// Instance IDs whose instance game settings must not be written back to disk + private final Set beingRemovedInstances = ConcurrentHashMap.newKeySet(); public final EventManager onInstanceIconChanged = new EventManager<>(); @@ -243,17 +246,43 @@ public void clean(GameInstanceID instanceId) throws IOException { clean(getRunDirectory(instanceId)); } + /// Marks the instance as being removed so that instance game settings changes + /// are not written back to disk + public void markInstanceBeingRemoved(GameInstanceID instanceId) { + beingRemovedInstances.add(instanceId); + } + + /// Unmarks the instance so that instance game settings can be saved again, + /// e.g. when the removal failed. + public void unmarkInstanceBeingRemoved(GameInstanceID instanceId) { + beingRemovedInstances.remove(instanceId); + } + /// Removes an instance from disk and clears its cached HMCL settings state. @Override public boolean removeInstanceFromDisk(GameInstanceID instanceId) { - boolean removed = super.removeInstanceFromDisk(instanceId); - if (removed) { - instanceGameSettings.remove(instanceId); - loadedInstanceGameSettings.remove(instanceId); - readOnlyInstanceGameSettings.remove(instanceId); - beingModpackInstances.remove(instanceId); + beingRemovedInstances.add(instanceId); + try { + // Flush settings saves that were enqueued before the removal started, so that they + // cannot recreate the instance directory after it has been moved + try { + FileSaver.waitForAllSaves(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + LOG.warning("Interrupted while waiting for pending file saves", e); + } + + boolean removed = super.removeInstanceFromDisk(instanceId); + if (removed) { + instanceGameSettings.remove(instanceId); + loadedInstanceGameSettings.remove(instanceId); + readOnlyInstanceGameSettings.remove(instanceId); + beingModpackInstances.remove(instanceId); + } + return removed; + } finally { + beingRemovedInstances.remove(instanceId); } - return removed; } public void duplicateInstance(GameInstanceID srcId, GameInstanceID dstId, boolean copySaves) throws IOException { @@ -680,7 +709,9 @@ else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE)) } public void saveGameSettings(GameInstanceID instanceId) { - if (!instanceGameSettings.containsKey(instanceId) || readOnlyInstanceGameSettings.contains(instanceId)) + if (beingRemovedInstances.contains(instanceId) + || !instanceGameSettings.containsKey(instanceId) + || readOnlyInstanceGameSettings.contains(instanceId)) return; GameSettings.Instance setting = instanceGameSettings.get(instanceId); if (setting == null) { @@ -705,7 +736,9 @@ public void saveGameSettings(GameInstanceID instanceId) { /// @param instanceId the instance ID /// @throws IOException if saving the file fails private void saveGameSettingsSync(GameInstanceID instanceId) throws IOException { - if (!instanceGameSettings.containsKey(instanceId) || readOnlyInstanceGameSettings.contains(instanceId)) { + if (beingRemovedInstances.contains(instanceId) + || !instanceGameSettings.containsKey(instanceId) + || readOnlyInstanceGameSettings.contains(instanceId)) { return; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java index 4d62e3263b3..31b007d4f95 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java @@ -128,8 +128,13 @@ public static void deleteInstance(HMCLGameRepository repository, GameInstanceID JFXButton deleteButton = new JFXButton(i18n("button.delete")); deleteButton.getStyleClass().add("dialog-error"); deleteButton.setOnAction(e -> { + // Block instance game settings from being written back to disk while the deletion + // task is running, otherwise a settings change during the deletion may recreate + // the instance directory after it has been moved to the trash + repository.markInstanceBeingRemoved(instanceId); Task.supplyAsync(Schedulers.io(), () -> repository.removeInstanceFromDisk(instanceId)) .whenComplete(Schedulers.javafx(), (result, exception) -> { + repository.unmarkInstanceBeingRemoved(instanceId); if (exception != null || !Boolean.TRUE.equals(result)) { Controllers.dialog(i18n("instance.manage.remove.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); }