Fix #6320: 修复安装新实例时,Fabric API 安装路径未遵循版本隔离策略的问题 (#6355)

This commit is contained in:
Glavo authored and GitHub committed 2026-07-10 20:36:29 +08:00
1 parent 98af2c85b6
commit 18dc5b69c0
7 files changed
+150 -23

No files matched your search

@@ -242,6 +242,19 @@ public final class HMCLGameRepository extends DefaultGameRepository {
clean(getRunDirectory(id));
}
/// Removes a version from disk and drops any cached instance settings for that version.
@Override
public boolean removeVersionFromDisk(String id) {
boolean removed = super.removeVersionFromDisk(id);
if (removed) {
instanceGameSettings.remove(id);
loadedInstanceGameSettings.remove(id);
readOnlyInstanceGameSettings.remove(id);
beingModpackVersions.remove(id);
}
return removed;
}
public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException {
Path srcDir = getVersionRoot(srcId);
Path dstDir = getVersionRoot(dstId);
@@ -521,31 +534,35 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
}
public GameSettings.Effective getEffectiveGameSettings(String id) {
GameSettings.Instance instance = getInstanceGameSettings(id);
return GameSettings.resolve(getParentGameSettings(instance), instance);
/// Returns whether a new instance should use an isolated running directory under the default isolation settings.
public boolean shouldIsolateNewInstance(boolean modded) {
GameSettings.Preset preset = getParentGameSettings(null);
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
return switch (type) {
case NEVER -> false;
case ALWAYS -> true;
case MODDED -> modded;
};
}
public void applyDefaultIsolationSetting(String id) {
if (!hasVersion(id)) {
/// Applies default isolation to a new instance before the version metadata is saved.
public void applyDefaultIsolationSettingForNewInstance(String id, boolean modded) {
if (!shouldIsolateNewInstance(modded) || readOnlyInstanceGameSettings.contains(id)) {
return;
}
GameSettings.Instance instanceSetting = getInstanceGameSettings(id);
GameSettings.Preset preset = getParentGameSettings(instanceSetting);
DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED);
boolean isolated = switch (type) {
case NEVER -> false;
case ALWAYS -> true;
case MODDED -> LibraryAnalyzer.isModded(this, getVersion(id).resolve(this));
};
if (isolated) {
GameSettings.Instance setting = instanceSetting != null ? instanceSetting : getInstanceGameSettingsOrCreate(id);
if (setting != null) {
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
}
GameSettings.Instance setting = getInstanceGameSettings(id);
if (setting == null) {
setting = initInstanceGameSettings(id, new GameSettings.Instance());
}
if (setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY)) {
saveGameSettings(id);
}
}
public GameSettings.Effective getEffectiveGameSettings(String id) {
GameSettings.Instance instance = getInstanceGameSettings(id);
return GameSettings.resolve(getParentGameSettings(instance), instance);
}
public Optional<Path> getVersionIconFile(String id) {
@@ -318,9 +318,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
builder.version(remoteVersion);
});
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
return builder.buildAsync().whenComplete(any -> {
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@@ -61,9 +61,9 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
builder.version(remoteVersion);
});
repository.applyDefaultIsolationSettingForNewInstance(name, settings.isInstallingModdedVersion());
return builder.buildAsync().whenComplete(any -> {
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
})
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@@ -408,6 +408,44 @@ public final class GameDirectoriesTest {
}
}
/// Tests that new isolated installing instances resolve content directories under the version root before metadata is saved.
@Test
public void newIsolatedInstallingInstanceUsesVersionRootBeforeVersionExists(@TempDir Path tempDirectory)
throws ReflectiveOperationException {
GameSettingsPresetID defaultPresetId =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174002");
GameSettings.Preset defaultPreset = new GameSettings.Preset(defaultPresetId);
defaultPreset.defaultIsolationTypeProperty().setValue(DefaultIsolationType.MODDED);
GameSettingsPresets presets = new GameSettingsPresets();
presets.getPresets().setAll(defaultPreset);
GameDirectory gameDirectory = new GameDirectory(
GameDirectoryID.generate(),
LocalizedText.plain("Dev"),
PortablePath.of(tempDirectory.toString()));
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(gameDirectory);
GameDirectories userDirectories = new GameDirectories();
try (GameDirectoryEnvironment ignored =
new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) {
settings().defaultGameSettingsPresetProperty().set(defaultPresetId);
HMCLGameRepository repository = new HMCLGameRepository(gameDirectory);
String id = "1.21.11-fabric";
assertFalse(repository.hasVersion(id));
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
repository.applyDefaultIsolationSettingForNewInstance(id, true);
assertEquals(repository.getVersionRoot(id), repository.getRunDirectory(id));
assertEquals(repository.getVersionRoot(id).resolve("mods"), repository.getModsDirectory(id));
assertTrue(repository.removeVersionFromDisk(id));
assertEquals(repository.getBaseDirectory(), repository.getRunDirectory(id));
}
}
/// Tests that instance settings without an explicit parent use the default preset.
@Test
public void nullInstanceParentUsesDefaultPresetInsteadOfLegacyGameDirectoryPreset()
@@ -233,7 +233,7 @@ public class DefaultGameRepository implements GameRepository {
public boolean removeVersionFromDisk(String id) {
if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id)) == Event.Result.DENY)
return false;
if (!versions.containsKey(id))
if (versions == null || !versions.containsKey(id))
return FileUtils.deleteDirectoryQuietly(getVersionRoot(id));
Path file = getVersionRoot(id);
if (Files.notExists(file))
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -25,7 +27,7 @@ import java.util.Map;
/// A wrapper for `Map<String, Object>`, supporting type-safe reading and writing of values.
///
/// @author Glavo
/// @author Glavo
public final class SettingsMap {
public record Key<T>(String key) {
}
@@ -87,6 +89,19 @@ public final class SettingsMap {
map.clear();
}
/// Returns whether the selected installation includes any non-vanilla component.
public boolean isInstallingModdedVersion() {
for (LibraryAnalyzer.LibraryType value : LibraryAnalyzer.LibraryType.values()) {
if (value != LibraryAnalyzer.LibraryType.MINECRAFT
&& value.isModLoader()
&& get(value.getPatchId()) instanceof RemoteVersion) {
return true;
}
}
return false;
}
@Override
public String toString() {
return "SettingsMap" + map;
@@ -0,0 +1,57 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2026 huangyuhui <[email protected]> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.util;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jetbrains.annotations.NotNullByDefault;
import org.junit.jupiter.api.Test;
import java.time.Instant;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/// Tests for installer state stored in [SettingsMap].
@NotNullByDefault
public final class SettingsMapTest {
/// Tests that vanilla game selection alone is not treated as a modded installation.
@Test
public void minecraftSelectionIsNotModdedInstallation() {
SettingsMap settings = new SettingsMap();
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), remoteVersion("game"));
assertFalse(settings.isInstallingModdedVersion());
}
/// Tests that selecting a non-vanilla installer is treated as a modded installation.
@Test
public void modLoaderSelectionIsModdedInstallation() {
SettingsMap settings = new SettingsMap();
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), remoteVersion("game"));
settings.put(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), remoteVersion("fabric"));
assertTrue(settings.isInstallingModdedVersion());
}
/// Creates a minimal remote version for installer state tests.
private static RemoteVersion remoteVersion(String libraryId) {
return new RemoteVersion(libraryId, "1.21.11", "test", Instant.EPOCH, List.of());
}
}