Skip to content

Commit 18dc5b6

Browse files
authored
Fix #6320: 修复安装新实例时,Fabric API 安装路径未遵循版本隔离策略的问题 (#6355)
1 parent 98af2c8 commit 18dc5b6

7 files changed

Lines changed: 150 additions & 23 deletions

File tree

HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,19 @@ public void clean(String id) throws IOException {
242242
clean(getRunDirectory(id));
243243
}
244244

245+
/// Removes a version from disk and drops any cached instance settings for that version.
246+
@Override
247+
public boolean removeVersionFromDisk(String id) {
248+
boolean removed = super.removeVersionFromDisk(id);
249+
if (removed) {
250+
instanceGameSettings.remove(id);
251+
loadedInstanceGameSettings.remove(id);
252+
readOnlyInstanceGameSettings.remove(id);
253+
beingModpackVersions.remove(id);
254+
}
255+
return removed;
256+
}
257+
245258
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
246259
Path srcDir = getVersionRoot(srcId);
247260
Path dstDir = getVersionRoot(dstId);
@@ -521,31 +534,35 @@ public GameSettings.Preset getParentGameSettings(@Nullable GameSettings.Instance
521534
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
522535
}
523536

524-
public GameSettings.Effective getEffectiveGameSettings(String id) {
525-
GameSettings.Instance instance = getInstanceGameSettings(id);
526-
return GameSettings.resolve(getParentGameSettings(instance), instance);
527-
}
528-
529-
public void applyDefaultIsolationSetting(String id) {
530-
if (!hasVersion(id)) {
531-
return;
532-
}
533-
534-
GameSettings.Instance instanceSetting = getInstanceGameSettings(id);
535-
GameSettings.Preset preset = getParentGameSettings(instanceSetting);
537+
/// Returns whether a new instance should use an isolated running directory under the default isolation settings.
538+
public boolean shouldIsolateNewInstance(boolean modded) {
539+
GameSettings.Preset preset = getParentGameSettings(null);
536540
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
537-
boolean isolated = switch (type) {
541+
return switch (type) {
538542
case NEVER -> false;
539543
case ALWAYS -> true;
540-
case MODDED -> LibraryAnalyzer.isModded(this, getVersion(id).resolve(this));
544+
case MODDED -> modded;
541545
};
546+
}
542547

543-
if (isolated) {
544-
GameSettings.Instance setting = instanceSetting != null ? instanceSetting : getInstanceGameSettingsOrCreate(id);
545-
if (setting != null) {
546-
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
547-
}
548+
/// Applies default isolation to a new instance before the version metadata is saved.
549+
public void applyDefaultIsolationSettingForNewInstance(String id, boolean modded) {
550+
if (!shouldIsolateNewInstance(modded) || readOnlyInstanceGameSettings.contains(id)) {
551+
return;
552+
}
553+
554+
GameSettings.Instance setting = getInstanceGameSettings(id);
555+
if (setting == null) {
556+
setting = initInstanceGameSettings(id, new GameSettings.Instance());
548557
}
558+
if (setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
559+
saveGameSettings(id);
560+
}
561+
}
562+
563+
public GameSettings.Effective getEffectiveGameSettings(String id) {
564+
GameSettings.Instance instance = getInstanceGameSettings(id);
565+
return GameSettings.resolve(getParentGameSettings(instance), instance);
549566
}
550567

551568
public Optional<Path> getVersionIconFile(String id) {

HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,9 @@ private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
318318
builder.version(remoteVersion);
319319
});
320320

321+
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
321322
return builder.buildAsync().whenComplete(any -> {
322323
repository.refreshVersions();
323-
repository.applyDefaultIsolationSetting(name);
324324
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
325325
}
326326

HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
6161
builder.version(remoteVersion);
6262
});
6363

64+
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
6465
return builder.buildAsync().whenComplete(any -> {
6566
repository.refreshVersions();
66-
repository.applyDefaultIsolationSetting(name);
6767
})
6868
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
6969
}

HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,44 @@ public void repositoryDirectoryFollowsGameDirectoryPath() throws ReflectiveOpera
408408
}
409409
}
410410

411+
/// Tests that new isolated installing instances resolve content directories under the version root before metadata is saved.
412+
@Test
413+
public void newIsolatedInstallingInstanceUsesVersionRootBeforeVersionExists(@TempDir Path tempDirectory)
414+
throws ReflectiveOperationException {
415+
GameSettingsPresetID defaultPresetId =
416+
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174002");
417+
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
418+
defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.MODDED);
419+
GameSettingsPresets presets = new GameSettingsPresets();
420+
presets.getPresets().setAll(defaultPreset);
421+
422+
GameDirectory gameDirectory = new GameDirectory(
423+
GameDirectoryID.generate(),
424+
LocalizedText.plain("Dev"),
425+
PortablePath.of(tempDirectory.toString()));
426+
GameDirectories localDirectories = new GameDirectories();
427+
localDirectories.getGameDirectories().add(gameDirectory);
428+
GameDirectories userDirectories = new GameDirectories();
429+
430+
try (GameDirectoryEnvironment ignored =
431+
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
432+
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
433+
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
434+
String id = "1.21.11-fabric";
435+
436+
assertFalse(repository.hasVersion(id));
437+
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
438+
439+
repository.applyDefaultIsolationSettingForNewInstance(id, true);
440+
441+
assertEquals(repository.getVersionRoot(id), repository.getRunDirectory(id));
442+
assertEquals(repository.getVersionRoot(id).resolve("mods"), repository.getModsDirectory(id));
443+
444+
assertTrue(repository.removeVersionFromDisk(id));
445+
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
446+
}
447+
}
448+
411449
/// Tests that instance settings without an explicit parent use the default preset.
412450
@Test
413451
public void nullInstanceParentUsesDefaultPresetInsteadOfLegacyGameDirectoryPreset()

HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ public boolean renameVersion(String from, String to) {
233233
public boolean removeVersionFromDisk(String id) {
234234
if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id)) == Event.Result.DENY)
235235
return false;
236-
if (!versions.containsKey(id))
236+
if (versions == null || !versions.containsKey(id))
237237
return FileUtils.deleteDirectoryQuietly(getVersionRoot(id));
238238
Path file = getVersionRoot(id);
239239
if (Files.notExists(file))

HMCLCore/src/main/java/org/jackhuang/hmcl/util/SettingsMap.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
*/
1818
package org.jackhuang.hmcl.util;
1919

20+
import org.jackhuang.hmcl.download.LibraryAnalyzer;
21+
import org.jackhuang.hmcl.download.RemoteVersion;
2022
import org.jetbrains.annotations.NotNull;
2123
import org.jetbrains.annotations.Nullable;
2224

@@ -25,7 +27,7 @@
2527

2628
/// A wrapper for `Map<String, Object>`, supporting type-safe reading and writing of values.
2729
///
28-
/// @author Glavo
30+
/// @author Glavo
2931
public final class SettingsMap {
3032
public record Key<T>(String key) {
3133
}
@@ -87,6 +89,19 @@ public void clear() {
8789
map.clear();
8890
}
8991

92+
/// Returns whether the selected installation includes any non-vanilla component.
93+
public boolean isInstallingModdedVersion() {
94+
for (LibraryAnalyzer.LibraryType value : LibraryAnalyzer.LibraryType.values()) {
95+
if (value != LibraryAnalyzer.LibraryType.MINECRAFT
96+
&& value.isModLoader()
97+
&& get(value.getPatchId()) instanceof RemoteVersion) {
98+
return true;
99+
}
100+
}
101+
102+
return false;
103+
}
104+
90105
@Override
91106
public String toString() {
92107
return "SettingsMap" + map;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Hello Minecraft! Launcher
3+
* Copyright (C) 2026 huangyuhui <huanghongxun2008@126.com> and contributors
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
package org.jackhuang.hmcl.util;
19+
20+
import org.jackhuang.hmcl.download.LibraryAnalyzer;
21+
import org.jackhuang.hmcl.download.RemoteVersion;
22+
import org.jetbrains.annotations.NotNullByDefault;
23+
import org.junit.jupiter.api.Test;
24+
25+
import java.time.Instant;
26+
import java.util.List;
27+
28+
import static org.junit.jupiter.api.Assertions.assertFalse;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
30+
31+
/// Tests for installer state stored in [SettingsMap].
32+
@NotNullByDefault
33+
public final class SettingsMapTest {
34+
/// Tests that vanilla game selection alone is not treated as a modded installation.
35+
@Test
36+
public void minecraftSelectionIsNotModdedInstallation() {
37+
SettingsMap settings = new SettingsMap();
38+
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), remoteVersion("game"));
39+
40+
assertFalse(settings.isInstallingModdedVersion());
41+
}
42+
43+
/// Tests that selecting a non-vanilla installer is treated as a modded installation.
44+
@Test
45+
public void modLoaderSelectionIsModdedInstallation() {
46+
SettingsMap settings = new SettingsMap();
47+
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), remoteVersion("game"));
48+
settings.put(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), remoteVersion("fabric"));
49+
50+
assertTrue(settings.isInstallingModdedVersion());
51+
}
52+
53+
/// Creates a minimal remote version for installer state tests.
54+
private static RemoteVersion remoteVersion(String libraryId) {
55+
return new RemoteVersion(libraryId, "1.21.11", "test", Instant.EPOCH, List.of());
56+
}
57+
}

0 commit comments

Comments
 (0)