重构游戏目录管理逻辑 (#6273)

This commit is contained in:
Glavo authored and GitHub committed 2026-06-30 20:21:50 +08:00
1 parent d8938e1fcb
commit 7580c6da82
65 files changed
+1651 -1527

No files matched your search

@@ -158,7 +158,7 @@ public final class Launcher extends Application {
DownloadProviders.init();
ProxyManager.init();
Accounts.init();
Profiles.init();
GameDirectoryManager.init();
AuthlibInjectorServers.init();
AnimationUtils.init();
@@ -20,8 +20,12 @@ package org.jackhuang.hmcl.game;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.StringBinding;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
@@ -33,10 +37,11 @@ import org.jackhuang.hmcl.modpack.ModpackProvider;
import org.jackhuang.hmcl.setting.LauncherSettings;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.setting.DefaultIsolationType;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.GameWindowType;
import org.jackhuang.hmcl.setting.LegacyGameSettingsMigrator;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.setting.ProxyType;
import org.jackhuang.hmcl.setting.SettingFileUtils;
import org.jackhuang.hmcl.setting.GameSettingsPresetID;
@@ -68,9 +73,17 @@ import static org.jackhuang.hmcl.setting.SettingsManager.settings;
import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/// HMCL game repository implementation backed by a profile and per-instance game settings.
/// HMCL game repository implementation backed by a GameDirectory and per-instance game settings.
@NotNullByDefault
public final class HMCLGameRepository extends DefaultGameRepository {
/// References an optional game instance in a repository.
///
/// @param repository the owning game repository
/// @param instanceId the game instance ID, or `null` when only repository context is available
@NotNullByDefault
public record InstanceReference(HMCLGameRepository repository, @Nullable String instanceId) {
}
/// Directory under the version root that stores HMCL-managed instance metadata.
private static final String INSTANCE_METADATA_DIRECTORY = ".hmcl";
@@ -83,7 +96,11 @@ public final class HMCLGameRepository extends DefaultGameRepository {
/// Current file name for instance-specific game settings.
private static final String INSTANCE_GAME_SETTINGS_FILENAME = "instance-game-settings.json";
private final Profile profile;
/// The persistent game directory for this repository.
private final GameDirectory gameDirectory;
/// The selected instance ID persisted for this repository's game directory.
private final StringBinding selectedInstance;
// instance game settings
private final Map<String, GameSettings.Instance> instanceGameSettings = new HashMap<>();
@@ -94,13 +111,54 @@ public final class HMCLGameRepository extends DefaultGameRepository {
public final EventManager<Event> onVersionIconChanged = new EventManager<>();
public HMCLGameRepository(Profile profile, Path baseDirectory) {
super(baseDirectory);
this.profile = profile;
/// Creates a repository backed by the given game directory.
public HMCLGameRepository(GameDirectory gameDirectory) {
super(gameDirectory.getPath().toPath());
this.gameDirectory = gameDirectory;
this.selectedInstance = Bindings.stringValueAt(settings().getSelectedInstance(), gameDirectory.getId());
gameDirectory.pathProperty().addListener((a, b, newValue) -> changeDirectory(newValue.toPath()));
}
public Profile getProfile() {
return profile;
/// Returns the persistent game directory for this repository.
public GameDirectory getGameDirectory() {
return gameDirectory;
}
/// Returns the selected instance ID property for this repository's game directory.
public StringBinding selectedInstanceProperty() {
return selectedInstance;
}
/// Returns the selected instance ID for this repository's game directory.
public @Nullable String getSelectedInstance() {
return selectedInstance.get();
}
/// Sets the selected instance ID for this repository's game directory.
public void setSelectedInstance(@Nullable String instance) {
settings().setSelectedInstance(gameDirectory.getId(), instance);
}
/// Refreshes the selected instance ID after versions are loaded.
public void refreshSelectedInstance() {
@Nullable String selectedInstance = settings().getSelectedInstance(gameDirectory.getId());
@Nullable String refreshedInstance = selectedInstance;
if (!hasVersion(refreshedInstance)) {
refreshedInstance = getVersions().isEmpty() ? null : getVersions().iterator().next().getId();
}
if (!Objects.equals(selectedInstance, refreshedInstance)) {
setSelectedInstance(refreshedInstance);
}
}
/// Returns a dependency manager using the currently selected download provider.
public DefaultDependencyManager getDependency() {
return getDependency(DownloadProviders.getDownloadProvider());
}
/// Returns a dependency manager using the given download provider.
public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) {
return new DefaultDependencyManager(this, downloadProvider, HMCLCacheRepository.REPOSITORY);
}
@Override
@@ -294,8 +352,8 @@ public final class HMCLGameRepository extends DefaultGameRepository {
return;
}
GameSettings.Preset profilePreset = SettingsManager.getGameSettings(profile.getLegacyGameSettings());
if (profilePreset != null && profilePreset.defaultIsolationTypeProperty().getValue() == DefaultIsolationType.ALWAYS) {
GameSettings.Preset gameDirectoryPreset = SettingsManager.getGameSettings(gameDirectory.getLegacyGameSettings());
if (gameDirectoryPreset != null && gameDirectoryPreset.defaultIsolationTypeProperty().getValue() == DefaultIsolationType.ALWAYS) {
GameSettings.Instance setting = new GameSettings.Instance();
setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY);
initInstanceGameSettings(id, setting);
@@ -461,7 +519,7 @@ public final class HMCLGameRepository extends DefaultGameRepository {
public GameSettings.Preset getParentGameSettings(@Nullable GameSettings.Instance instance) {
@Nullable GameSettingsPresetID parent = instance != null && instance.parentProperty().getValue() != null
? instance.parentProperty().getValue()
: profile.getLegacyGameSettings();
: gameDirectory.getLegacyGameSettings();
GameSettings.Preset parentSetting = SettingsManager.getGameSettings(parent);
return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate();
}
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.modpack.MinecraftInstanceTask;
import org.jackhuang.hmcl.modpack.Modpack;
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
import org.jackhuang.hmcl.modpack.ModpackInstallTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.CompressingUtils;
@@ -45,9 +44,9 @@ public final class HMCLModpackInstallTask extends Task<Void> {
private final List<Task<?>> dependencies = new ArrayList<>(1);
private final List<Task<?>> dependents = new ArrayList<>(4);
public HMCLModpackInstallTask(Profile profile, Path zipFile, Modpack modpack, String name) {
dependency = profile.getDependency();
repository = profile.getRepository();
public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpack modpack, String name) {
this.repository = repository;
this.dependency = repository.getDependency();
this.zipFile = zipFile;
this.name = name;
this.modpack = modpack;
@@ -24,7 +24,6 @@ import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException;
import org.jackhuang.hmcl.modpack.Modpack;
import org.jackhuang.hmcl.modpack.ModpackProvider;
import org.jackhuang.hmcl.modpack.ModpackUpdateTask;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
@@ -56,9 +55,7 @@ public final class HMCLModpackProvider implements ModpackProvider {
throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository");
}
Profile profile = repository.getProfile();
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(profile, zipFile, modpack, name));
return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(repository, zipFile, modpack, name));
}
@Override
@@ -80,7 +77,7 @@ public final class HMCLModpackProvider implements ModpackProvider {
private final static class HMCLModpack extends Modpack {
@Override
public Task<?> getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) {
return new HMCLModpackInstallTask(((HMCLGameRepository) dependencyManager.getGameRepository()).getProfile(), zipFile, this, name);
return new HMCLModpackInstallTask((HMCLGameRepository) dependencyManager.getGameRepository(), zipFile, this, name);
}
}
@@ -77,7 +77,7 @@ public final class LauncherHelper {
private static final String LWJGL_3_4_1_TIP = "lwjgl3.4.1-ffm";
private final Profile profile;
private final HMCLGameRepository repository;
private Account account;
private final String selectedVersion;
private Path scriptFile;
@@ -87,11 +87,11 @@ public final class LauncherHelper {
private QuickPlayOption quickPlayOption;
private boolean disableOfflineSkin = false;
public LauncherHelper(Profile profile, Account account, String selectedVersion) {
this.profile = Objects.requireNonNull(profile);
public LauncherHelper(HMCLGameRepository repository, Account account, String selectedVersion) {
this.repository = Objects.requireNonNull(repository);
this.account = Objects.requireNonNull(account);
this.selectedVersion = Objects.requireNonNull(selectedVersion);
this.setting = profile.getRepository().getEffectiveGameSettings(selectedVersion);
this.setting = repository.getEffectiveGameSettings(selectedVersion);
this.launcherVisibility = setting.getInheritable(GameSettings::launcherVisibilityProperty);
this.showLogs = setting.getInheritable(GameSettings::showLogsProperty);
this.launchingStepsPane.setTitle(i18n("version.launch"));
@@ -142,8 +142,7 @@ public final class LauncherHelper {
// https://github.com/HMCL-dev/HMCL/pull/4121
PROCESSES.removeIf(it -> it.get() == null);
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = profile.getDependency();
DefaultDependencyManager dependencyManager = repository.getDependency();
AtomicReference<Version> version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion)));
Optional<String> gameVersion = repository.getGameVersion(version.get());
boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion);
@@ -153,7 +152,7 @@ public final class LauncherHelper {
AtomicReference<JavaRuntime> javaVersionRef = new AtomicReference<>();
TaskExecutor executor = checkGameState(profile, setting, version.get())
TaskExecutor executor = checkGameState(repository, setting, version.get())
.thenComposeAsync(java -> {
javaVersionRef.set(Objects.requireNonNull(java));
version.set(NativePatcher.patchNative(repository, version.get(), gameVersion.orElse(null), java, setting, javaArguments));
@@ -224,7 +223,7 @@ public final class LauncherHelper {
.thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in"))
.thenComposeAsync(authInfo -> Task.supplyAsync(() -> {
LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions(
selectedVersion, javaVersionRef.get(), profile.getPath().toPath(), javaAgents, javaArguments, scriptFile != null);
selectedVersion, javaVersionRef.get(), repository.getBaseDirectory(), javaAgents, javaArguments, scriptFile != null);
if (disableOfflineSkin) {
launchOptionsBuilder.setDaemon(false);
}
@@ -382,8 +381,8 @@ public final class LauncherHelper {
executor.start();
}
private static Task<JavaRuntime> checkGameState(Profile profile, GameSettings.Effective setting, Version version) {
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, profile.getRepository().getGameVersion(version).orElse(null));
private static Task<JavaRuntime> checkGameState(HMCLGameRepository repository, GameSettings.Effective setting, Version version) {
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null));
GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT));
Task<JavaRuntime> getJavaTask = Task.supplyAsync(() -> {
@@ -450,7 +449,7 @@ public final class LauncherHelper {
}
if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) {
downloadJava(targetJavaVersion, profile)
downloadJava(targetJavaVersion, repository)
.whenCompleteAsync((downloadedJava, exception) -> {
if (exception == null) {
future.complete(downloadedJava);
@@ -527,7 +526,7 @@ public final class LauncherHelper {
gameJavaVersion = null;
if (gameJavaVersion != null) {
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, profile).whenCompleteAsync((downloadedJava, throwable) -> {
FXUtils.runInFX(() -> downloadJava(gameJavaVersion, repository).whenCompleteAsync((downloadedJava, throwable) -> {
if (throwable == null) {
setting.setJavaAutoSelected();
future.complete(downloadedJava);
@@ -685,14 +684,14 @@ public final class LauncherHelper {
return task.withStage("launch.state.java");
}
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, Profile profile) {
private static CompletableFuture<JavaRuntime> downloadJava(GameJavaVersion javaVersion, HMCLGameRepository repository) {
CompletableFuture<JavaRuntime> future = new CompletableFuture<>();
Controllers.dialog(new MessageDialogPane.Builder(
i18n("launch.advice.require_newer_java_version", javaVersion.majorVersion()),
i18n("message.warning"),
MessageType.QUESTION)
.yesOrNo(() -> {
DownloadProvider downloadProvider = profile.getDependency().getDownloadProvider();
DownloadProvider downloadProvider = repository.getDependency().getDownloadProvider();
Controllers.taskDialog(JavaManager.getDownloadJavaTask(downloadProvider, SYSTEM_PLATFORM, javaVersion)
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
@@ -17,7 +17,6 @@
*/
package org.jackhuang.hmcl.game;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.io.CompressingUtils;
import org.jackhuang.hmcl.util.io.Unzipper;
@@ -29,13 +28,11 @@ import java.nio.file.Paths;
public class ManuallyCreatedModpackInstallTask extends Task<Path> {
private final Profile profile;
private final Path zipFile;
private final Charset charset;
private final String name;
public ManuallyCreatedModpackInstallTask(Profile profile, Path zipFile, Charset charset, String name) {
this.profile = profile;
public ManuallyCreatedModpackInstallTask(Path zipFile, Charset charset, String name) {
this.zipFile = zipFile;
this.charset = charset;
this.name = name;
@@ -34,8 +34,8 @@ import org.jackhuang.hmcl.modpack.server.ServerModpackRemoteInstallTask;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.GameWindowType;
import org.jackhuang.hmcl.setting.JavaVersionType;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
@@ -156,11 +156,10 @@ public final class ModpackHelper {
}
}
public static Task<?> getInstallTask(Profile profile, ServerModpackManifest manifest, String name, Modpack modpack) {
profile.getRepository().markVersionAsModpack(name);
public static Task<?> getInstallTask(HMCLGameRepository repository, ServerModpackManifest manifest, String name, Modpack modpack) {
repository.markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
HMCLGameRepository repository = profile.getRepository();
repository.refreshVersions();
GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(name);
repository.undoMark(name);
@@ -176,7 +175,7 @@ public final class ModpackHelper {
}
};
return new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name)
return new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, name)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
@@ -185,27 +184,26 @@ public final class ModpackHelper {
return Files.exists(Paths.get("externalgames").resolve(name));
}
public static Task<?> getInstallManuallyCreatedModpackTask(Profile profile, Path zipFile, String name, Charset charset) {
public static Task<?> getInstallManuallyCreatedModpackTask(Path zipFile, String name, Charset charset) {
if (isExternalGameNameConflicts(name)) {
throw new IllegalArgumentException("name existing");
}
return new ManuallyCreatedModpackInstallTask(profile, zipFile, charset, name)
return new ManuallyCreatedModpackInstallTask(zipFile, charset, name)
.thenAcceptAsync(Schedulers.javafx(), location -> {
Profile newProfile = new Profile(
Profiles.newProfileId(),
GameDirectory newGameDirectory = new GameDirectory(
GameDirectoryManager.newGameDirectoryId(),
LocalizedText.plain(name),
PortablePath.fromPath(location));
Profiles.addLocalProfile(newProfile);
Profiles.setSelectedProfile(newProfile);
GameDirectoryManager.addLocalGameDirectory(newGameDirectory);
GameDirectoryManager.setSelectedGameDirectory(newGameDirectory);
});
}
public static Task<?> getInstallTask(Profile profile, Path zipFile, String name, Modpack modpack, String iconUrl) {
profile.getRepository().markVersionAsModpack(name);
public static Task<?> getInstallTask(HMCLGameRepository repository, Path zipFile, String name, Modpack modpack, String iconUrl) {
repository.markVersionAsModpack(name);
ExceptionalRunnable<?> success = () -> {
HMCLGameRepository repository = profile.getRepository();
repository.refreshVersions();
GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(name);
repository.undoMark(name);
@@ -222,45 +220,45 @@ public final class ModpackHelper {
};
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMultiMCPostInstallTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else if (modpack.getManifest() instanceof McbbsModpackManifest)
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.defaultScheduler(), success, failure)
.thenComposeAsync(createMcbbsPostInstallTask(profile, (McbbsModpackManifest) modpack.getManifest(), name))
.thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), name))
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
else
return modpack.getInstallTask(profile.getDependency(), zipFile, name, iconUrl)
return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl)
.whenComplete(Schedulers.javafx(), success, failure)
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
}
public static Task<Void> getUpdateTask(Profile profile, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
public static Task<Void> getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException {
switch (configuration.getType()) {
case ServerModpackRemoteInstallTask.MODPACK_TYPE:
return new ModpackUpdateTask(profile.getRepository(), name, new ServerModpackRemoteInstallTask(profile.getDependency(), manifest, name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
return new ModpackUpdateTask(repository, name, new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, name))
.thenComposeAsync(repository.refreshVersionsAsync())
.withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries")));
default:
throw new UnsupportedModpackException();
}
}
public static Task<?> getUpdateTask(Profile profile, Path zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
public static Task<?> getUpdateTask(HMCLGameRepository repository, Path zipFile, Charset charset, String name, ModpackConfiguration<?> configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException {
Modpack modpack = ModpackHelper.readModpackManifest(zipFile, charset);
ModpackProvider provider = getProviderByType(configuration.getType());
if (provider == null) {
throw new UnsupportedModpackException();
}
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
return provider.createUpdateTask(repository.getDependency(), name, zipFile, modpack)
.thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), name))
.thenComposeAsync(repository.refreshVersionsAsync());
else
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
return provider.createUpdateTask(repository.getDependency(), name, zipFile, modpack)
.thenComposeAsync(repository.refreshVersionsAsync());
}
public static void toGameSettings(MultiMCInstanceConfiguration c, GameSettings.Instance setting) {
@@ -335,23 +333,22 @@ public final class ModpackHelper {
}
}
private static Task<Void> createMultiMCPostUpdateTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
private static Task<Void> createMultiMCPostUpdateTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
GameSettings.Instance setting = Objects.requireNonNull(profile.getRepository().getInstanceGameSettingsOrCreate(version));
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(version));
ModpackHelper.applyCommandAndJvmSettings(manifest, setting);
});
}
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
private static Task<Void> createMultiMCPostInstallTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
GameSettings.Instance setting = Objects.requireNonNull(profile.getRepository().getInstanceGameSettingsOrCreate(version));
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(version));
ModpackHelper.toGameSettings(manifest, setting);
});
}
private static Task<Void> createMcbbsPostInstallTask(Profile profile, McbbsModpackManifest manifest, String version) {
private static Task<Void> createMcbbsPostInstallTask(HMCLGameRepository repository, McbbsModpackManifest manifest, String version) {
return Task.runAsync(Schedulers.javafx(), () -> {
HMCLGameRepository repository = profile.getRepository();
GameSettings.Effective effective = repository.getEffectiveGameSettings(version);
if (manifest.getLaunchInfo().getMinMemory() > effective.getMaxMemory()) {
GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(version));
@@ -102,14 +102,14 @@ public final class GameDirectories extends ObservableSetting implements JsonSche
/// Game directories stored in this file.
@SerializedName("directories")
private final ObservableList<Profile> gameDirectories =
private final ObservableList<GameDirectory> gameDirectories =
FXCollections.observableArrayList(profile -> new Observable[] { profile });
/// Whether this store represents `HMCL_USER_HOME/config/user-game-directories.json`.
private transient boolean userFile;
/// Returns the game directories stored in this file.
public ObservableList<Profile> getGameDirectories() {
public ObservableList<GameDirectory> getGameDirectories() {
return gameDirectories;
}
@@ -24,10 +24,6 @@ import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.HMCLCacheRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.util.PortablePath;
import org.jackhuang.hmcl.util.ToStringBuilder;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
@@ -40,15 +36,10 @@ import java.util.Objects;
import static org.jackhuang.hmcl.ui.FXUtils.onInvalidating;
/**
*
* @author huangyuhui
*/
@JsonAdapter(Profile.Serializer.class)
/// Persistent configuration for a game directory.
@JsonAdapter(GameDirectory.Serializer.class)
@NotNullByDefault
public final class Profile implements Observable {
private final HMCLGameRepository repository;
public final class GameDirectory implements Observable {
/// The stable game directory ID.
private final GameDirectoryID id;
@@ -75,25 +66,25 @@ public final class Profile implements Observable {
this.path.set(Objects.requireNonNull(path));
}
/// The custom localized profile name, or `null` for profiles without a stored name.
/// The custom localized game directory name, or `null` when no name is stored.
private final ObjectProperty<@Nullable LocalizedText> name;
/// Returns the custom localized profile name property.
/// Returns the custom localized game directory name property.
public ObjectProperty<@Nullable LocalizedText> nameProperty() {
return name;
}
/// Returns the custom localized profile name, or `null` when no name is stored.
/// Returns the custom localized game directory name, or `null` when no name is stored.
public @Nullable LocalizedText getName() {
return name.get();
}
/// Sets the custom localized profile name.
/// Sets the custom localized game directory name.
public void setName(@Nullable LocalizedText name) {
this.name.set(name);
}
/// The migrated legacy game settings preset ID, or `null` when this profile uses the default preset.
/// The migrated legacy game settings preset ID, or `null` when this game directory uses the default preset.
private final ObjectProperty<@Nullable GameSettingsPresetID> legacyGameSettings;
/// Returns the migrated legacy game settings preset ID property.
@@ -101,7 +92,7 @@ public final class Profile implements Observable {
return legacyGameSettings;
}
/// Returns the migrated legacy game settings preset ID, or `null` when this profile uses the default preset.
/// Returns the migrated legacy game settings preset ID, or `null` when this game directory uses the default preset.
public @Nullable GameSettingsPresetID getLegacyGameSettings() {
return legacyGameSettings.get();
}
@@ -111,13 +102,13 @@ public final class Profile implements Observable {
this.legacyGameSettings.set(legacyGameSettings);
}
/// Creates a profile.
public Profile(GameDirectoryID id, @Nullable LocalizedText name, PortablePath path) {
/// Creates a game directory.
public GameDirectory(GameDirectoryID id, @Nullable LocalizedText name, PortablePath path) {
this(id, name, path, null);
}
/// Creates a profile.
public Profile(
/// Creates a game directory.
public GameDirectory(
GameDirectoryID id,
@Nullable LocalizedText name,
PortablePath path,
@@ -126,25 +117,11 @@ public final class Profile implements Observable {
this.name = new SimpleObjectProperty<>(this, "name", name);
this.path = new SimpleObjectProperty<>(this, "path", Objects.requireNonNull(path));
this.legacyGameSettings = new SimpleObjectProperty<>(this, "legacyGameSettings", legacyGameSettings);
repository = new HMCLGameRepository(this, path.toPath());
this.path.addListener((a, b, newValue) -> repository.changeDirectory(newValue.toPath()));
addPropertyChangedListener(onInvalidating(this::invalidate));
}
public HMCLGameRepository getRepository() {
return repository;
}
public DefaultDependencyManager getDependency() {
return getDependency(DownloadProviders.getDownloadProvider());
}
public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) {
return new DefaultDependencyManager(repository, downloadProvider, HMCLCacheRepository.REPOSITORY);
}
/// Returns a debug string containing the game directory path and display metadata.
@Override
public String toString() {
return new ToStringBuilder(this)
@@ -153,25 +130,29 @@ public final class Profile implements Observable {
.toString();
}
/// Registers a listener that invalidates this game directory when any stored property changes.
private void addPropertyChangedListener(InvalidationListener listener) {
name.addListener(listener);
path.addListener(listener);
legacyGameSettings.addListener(listener);
}
/// Helper that stores and dispatches invalidation listeners for this game directory.
private final ObservableHelper observableHelper = new ObservableHelper(this);
/// Adds an invalidation listener.
@Override
public void addListener(InvalidationListener listener) {
observableHelper.addListener(listener);
}
/// Removes an invalidation listener.
@Override
public void removeListener(InvalidationListener listener) {
observableHelper.removeListener(listener);
}
/// Notifies profile observers on the JavaFX thread when the toolkit is available.
/// Notifies game directory observers on the JavaFX thread when the toolkit is available.
private void invalidate() {
try {
Platform.runLater(observableHelper::invalidate);
@@ -180,12 +161,12 @@ public final class Profile implements Observable {
}
}
public record ProfileVersion(Profile profile, @Nullable String version) {
}
public static final class Serializer implements JsonSerializer<Profile>, JsonDeserializer<Profile> {
/// Serializes and deserializes game directories.
@NotNullByDefault
public static final class Serializer implements JsonSerializer<GameDirectory>, JsonDeserializer<GameDirectory> {
/// Serializes a game directory to JSON.
@Override
public JsonElement serialize(@Nullable Profile src, Type typeOfSrc, JsonSerializationContext context) {
public JsonElement serialize(@Nullable GameDirectory src, Type typeOfSrc, JsonSerializationContext context) {
if (src == null)
return JsonNull.INSTANCE;
@@ -205,8 +186,9 @@ public final class Profile implements Observable {
return jsonObject;
}
/// Deserializes a game directory from JSON.
@Override
public @Nullable Profile deserialize(@Nullable JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
public @Nullable GameDirectory deserialize(@Nullable JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (!(json instanceof JsonObject obj)) return null;
GameDirectoryID id = context.deserialize(obj.get("id"), GameDirectoryID.class);
if (id == null) {
@@ -216,11 +198,11 @@ public final class Profile implements Observable {
}
PortablePath path = context.deserialize(obj.get("path"), PortablePath.class);
if (path == null) {
throw new JsonParseException("Profile path cannot be null");
throw new JsonParseException("Game directory path cannot be null");
}
@Nullable LocalizedText name = context.deserialize(obj.get("name"), LocalizedText.class);
return new Profile(id,
return new GameDirectory(id,
name,
path,
context.deserialize(obj.get("legacyGameSettings"), GameSettingsPresetID.class));
@@ -0,0 +1,493 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 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.setting;
import javafx.beans.property.*;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.util.PortablePath;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.jetbrains.annotations.UnmodifiableView;
import java.util.ArrayList;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.SettingsManager.*;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/// Manages the merged runtime view of local and user game directories.
///
/// Local game directories take precedence over user game directories with the same ID. This class is
/// the owner of the currently selected game directory and selected instance bookkeeping;
/// callers should use this API instead of mutating the game-directory selection fields in
/// [LauncherSettings] directly.
@NotNullByDefault
public final class GameDirectoryManager {
/// The default current-workspace game directory path.
private static final PortablePath CURRENT_GAME_DIRECTORY_PATH = PortablePath.of(".minecraft");
/// The default user-home game directory path.
private static final PortablePath HOME_GAME_DIRECTORY_PATH = PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY);
/// Prevents instantiation.
private GameDirectoryManager() {
}
/// Creates a game directory ID that does not collide with existing game directories.
public static GameDirectoryID newGameDirectoryId() {
GameDirectoryID id;
do {
id = GameDirectoryID.generate();
} while (hasGameDirectoryId(id));
return id;
}
/// Returns whether an existing game directory uses the given ID.
private static boolean hasGameDirectoryId(GameDirectoryID id) {
return localGameDirectories().getGameDirectories().stream()
.anyMatch(gameDirectory -> gameDirectory.getId().equals(id))
|| userGameDirectories().getGameDirectories().stream()
.anyMatch(gameDirectory -> gameDirectory.getId().equals(id));
}
/// Returns the display name for the given game directory.
public static String getGameDirectoryDisplayName(GameDirectory gameDirectory) {
String name = getGameDirectoryCustomName(gameDirectory);
if (name != null) {
return name;
}
if (isGameDirectoryPath(gameDirectory, CURRENT_GAME_DIRECTORY_PATH)) {
return i18n("game_directory.default");
}
if (isGameDirectoryPath(gameDirectory, HOME_GAME_DIRECTORY_PATH)) {
return i18n("game_directory.home");
}
return gameDirectory.getId().toString();
}
/// Returns the custom game directory name in the current locale.
public static @Nullable String getGameDirectoryCustomName(GameDirectory gameDirectory) {
@Nullable LocalizedText name = gameDirectory.getName();
return name != null ? name.getText(I18n.getLocale().getCandidateLocales()) : null;
}
/// Returns whether the game directory uses the given path.
private static boolean isGameDirectoryPath(GameDirectory gameDirectory, PortablePath expectedPath) {
PortablePath actualPath = gameDirectory.getPath();
return actualPath.isAbsolute() == expectedPath.isAbsolute()
&& actualPath.getPath().equals(expectedPath.getPath());
}
/// True if [#init()] hasn't been called.
private static boolean initialized = false;
/// The mutable merged game directory list used by UI bindings and game directory selection.
private static final ObservableList<GameDirectory> mergedGameDirectories =
FXCollections.observableArrayList(gameDirectory -> new javafx.beans.Observable[]{gameDirectory});
/// Read-only view of [#mergedGameDirectories] exposed to callers.
private static final @UnmodifiableView ObservableList<GameDirectory> mergedGameDirectoriesUnmodifiable =
FXCollections.unmodifiableObservableList(mergedGameDirectories);
/// Runtime repositories keyed by game directory object identity.
private static final Map<GameDirectory, HMCLGameRepository> repositories = new IdentityHashMap<>();
/// The selected game directory, or `null` before the fallback game directory is resolved.
private static final ObjectProperty<@UnknownNullability GameDirectory> selectedGameDirectory = new SimpleObjectProperty<>(GameDirectoryManager.class, "selectedGameDirectory");
/// The selected game repository, or `null` before the fallback game directory is resolved.
private static final ObjectProperty<@UnknownNullability HMCLGameRepository> selectedRepository = new SimpleObjectProperty<>(GameDirectoryManager.class, "selectedRepository");
/// The selected instance ID projected from the selected repository.
private static final ReadOnlyStringWrapper selectedInstance = new ReadOnlyStringWrapper(GameDirectoryManager.class, "selectedInstance");
/// Updates [#selectedInstance] when the selected repository changes its selected instance.
private static final ChangeListener<String> selectedRepositoryInstanceListener =
(observable, oldValue, newValue) -> selectedInstance.set(newValue);
/// Initializes game directory state from the stores loaded by [SettingsManager].
///
/// This method creates the built-in local and user-home game directories when required, rebuilds
/// the merged game directory view, and restores the selected game directory from [LauncherSettings].
public static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
initialized = true;
rebuildGameDirectories();
boolean needRebuildGameDirectories = false;
if (localGameDirectories().isNewlyCreated() && localGameDirectories().getGameDirectories().isEmpty()) {
needRebuildGameDirectories = true;
GameDirectories gameDirectories = localGameDirectories();
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
}
if (userGameDirectories().isNewlyCreated() && userGameDirectories().getGameDirectories().isEmpty()) {
needRebuildGameDirectories = true;
GameDirectories gameDirectories = userGameDirectories();
gameDirectories.getGameDirectories().add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
}
needRebuildGameDirectories |= createDefaultGameDirectoriesIfEmpty();
if (needRebuildGameDirectories) {
rebuildGameDirectories();
}
assert !mergedGameDirectories.isEmpty();
@Nullable GameDirectoryID selectedId = settings().selectedGameDirectoryProperty().get();
GameDirectory currentGameDirectory = null;
if (selectedId != null) {
for (GameDirectory gameDirectory : mergedGameDirectories) {
if (gameDirectory.getId().equals(selectedId)) {
currentGameDirectory = gameDirectory;
break;
}
}
}
selectedGameDirectory.addListener((a, b, newValue) -> {
if (newValue == null) {
throw new IllegalStateException("selectedGameDirectory cannot be null");
}
settings().selectedGameDirectoryProperty().set(newValue.getId());
@Nullable HMCLGameRepository oldRepository = selectedRepository.get();
if (oldRepository != null) {
oldRepository.selectedInstanceProperty().removeListener(selectedRepositoryInstanceListener);
}
HMCLGameRepository repository = getOrCreateRepository(newValue);
selectedRepository.set(repository);
selectedInstance.set(repository.getSelectedInstance());
repository.selectedInstanceProperty().addListener(selectedRepositoryInstanceListener);
repository.refreshVersionsAsync().start();
});
selectedGameDirectory.set(currentGameDirectory != null ? currentGameDirectory : mergedGameDirectories.get(0));
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
runInFX(() -> {
@Nullable HMCLGameRepository repository = selectedRepository.get();
if (repository != null && repository == event.getSource()) {
repository.refreshSelectedInstance();
for (Consumer<HMCLGameRepository> listener : versionsListeners)
listener.accept(repository);
}
});
});
}
/// Creates the built-in game directories only when no game directory exists.
private static boolean createDefaultGameDirectoriesIfEmpty() {
if (localGameDirectories().getGameDirectories().isEmpty()
&& userGameDirectories().getGameDirectories().isEmpty()) {
localGameDirectories().getGameDirectories()
.add(new GameDirectory(newGameDirectoryId(), null, CURRENT_GAME_DIRECTORY_PATH));
userGameDirectories().getGameDirectories()
.add(new GameDirectory(newGameDirectoryId(), null, HOME_GAME_DIRECTORY_PATH));
return true;
} else {
return false;
}
}
/// Rebuilds the merged runtime game directory view from the two backing stores.
private static void rebuildGameDirectories() {
GameDirectories userGameDirectories = userGameDirectories();
GameDirectories localGameDirectories = localGameDirectories();
Map<GameDirectoryID, GameDirectory> visibleGameDirectories = new LinkedHashMap<>();
for (GameDirectory gameDirectory : localGameDirectories.getGameDirectories()) {
GameDirectoryID id = gameDirectory.getId();
visibleGameDirectories.put(id, gameDirectory);
}
for (GameDirectory gameDirectory : userGameDirectories.getGameDirectories()) {
GameDirectoryID id = gameDirectory.getId();
visibleGameDirectories.putIfAbsent(id, gameDirectory);
}
mergedGameDirectories.setAll(visibleGameDirectories.values());
}
/// Returns the read-only merged game directory list.
public static @UnmodifiableView ObservableList<GameDirectory> getGameDirectories() {
return mergedGameDirectoriesUnmodifiable;
}
/// Returns the repository for the given game directory, creating it when needed.
private static HMCLGameRepository getOrCreateRepository(GameDirectory gameDirectory) {
Objects.requireNonNull(gameDirectory);
return repositories.computeIfAbsent(gameDirectory, HMCLGameRepository::new);
}
/// Adds a game directory to the per-workspace store.
public static void addLocalGameDirectory(GameDirectory gameDirectory) {
if (SettingsManager.isLocalGameDirectoriesReadOnly()) {
throw new IllegalStateException("Local game directories are read-only");
}
addGameDirectory(localGameDirectories(), gameDirectory);
}
/// Adds a game directory to the user store.
public static void addUserGameDirectory(GameDirectory gameDirectory) {
if (SettingsManager.isUserGameDirectoriesReadOnly()) {
throw new IllegalStateException("User game directories are read-only");
}
addGameDirectory(userGameDirectories(), gameDirectory);
}
/// Returns whether an existing game directory can be updated to the given path.
///
/// @param gameDirectory the game directory to update
/// @param path the new game directory path
/// @return whether both the current and target stores may be edited
public static boolean canUpdateGameDirectory(GameDirectory gameDirectory, PortablePath path) {
GameDirectories source = requireGameDirectoryStore(gameDirectory);
GameDirectories target = getGameDirectoryStore(path);
return !isGameDirectoriesReadOnly(source) && !isGameDirectoriesReadOnly(target);
}
/// Backs up and overwrites read-only game directory files required to update a game directory.
///
/// @param gameDirectory the game directory to update
/// @param path the new game directory path
public static void forceOverwriteGameDirectoryFiles(GameDirectory gameDirectory, PortablePath path) {
GameDirectories source = requireGameDirectoryStore(gameDirectory);
GameDirectories target = getGameDirectoryStore(path);
forceOverwriteGameDirectoriesIfReadOnly(source);
if (target != source) {
forceOverwriteGameDirectoriesIfReadOnly(target);
}
}
/// Updates a game directory, moving it between local and user stores when the path type changes.
///
/// @param gameDirectory the game directory to update
/// @param name the new custom game directory name, or `null` for an unnamed game directory
/// @param path the new game directory path
public static void updateGameDirectory(GameDirectory gameDirectory, @Nullable LocalizedText name, PortablePath path) {
GameDirectories source = requireGameDirectoryStore(gameDirectory);
GameDirectories target = getGameDirectoryStore(path);
if (isGameDirectoriesReadOnly(source) || isGameDirectoriesReadOnly(target)) {
throw new IllegalStateException("Game directories are read-only");
}
if (source == target) {
gameDirectory.setName(name);
gameDirectory.setPath(path);
return;
}
source.getGameDirectories().remove(gameDirectory);
gameDirectory.setName(name);
gameDirectory.setPath(path);
addGameDirectory(target, gameDirectory);
}
/// Returns whether the game directory can be removed from all stores containing it.
///
/// @param gameDirectory the game directory to remove
/// @return whether all containing stores may be edited
public static boolean canRemoveGameDirectory(GameDirectory gameDirectory) {
boolean localContains = localGameDirectories().getGameDirectories().contains(gameDirectory);
boolean userContains = userGameDirectories().getGameDirectories().contains(gameDirectory);
return (!localContains || !SettingsManager.isLocalGameDirectoriesReadOnly())
&& (!userContains || !SettingsManager.isUserGameDirectoriesReadOnly());
}
/// Backs up and overwrites read-only game directory files required to remove a game directory.
///
/// @param gameDirectory the game directory to remove
public static void forceOverwriteGameDirectoryFiles(GameDirectory gameDirectory) {
if (localGameDirectories().getGameDirectories().contains(gameDirectory)) {
forceOverwriteGameDirectoriesIfReadOnly(localGameDirectories());
}
if (userGameDirectories().getGameDirectories().contains(gameDirectory)) {
forceOverwriteGameDirectoriesIfReadOnly(userGameDirectories());
}
}
/// Adds a game directory to the given store, replacing a game directory with the same ID in that store.
private static void addGameDirectory(GameDirectories gameDirectories, GameDirectory gameDirectory) {
Objects.requireNonNull(gameDirectory);
ObservableList<GameDirectory> entries = gameDirectories.getGameDirectories();
GameDirectoryID id = gameDirectory.getId();
for (int i = 0; i < entries.size(); i++) {
if (entries.get(i).getId().equals(id)) {
repositories.remove(entries.get(i));
settings().setSelectedInstance(id, null);
entries.set(i, gameDirectory);
rebuildGameDirectories();
return;
}
}
entries.add(gameDirectory);
rebuildGameDirectories();
}
/// Removes a game directory and recreates the built-in game directories when the list becomes empty.
public static void removeGameDirectory(GameDirectory gameDirectory) {
if (!canRemoveGameDirectory(gameDirectory)) {
throw new IllegalStateException("Game directories are read-only");
}
userGameDirectories().getGameDirectories().remove(gameDirectory);
localGameDirectories().getGameDirectories().remove(gameDirectory);
repositories.remove(gameDirectory);
createDefaultGameDirectoriesIfEmpty();
rebuildGameDirectories();
if (!mergedGameDirectories.contains(selectedGameDirectory.get())) {
setSelectedGameDirectory(mergedGameDirectories.get(0));
}
}
/// Returns the store that should own game directories with the given path.
private static GameDirectories getGameDirectoryStore(PortablePath path) {
Objects.requireNonNull(path);
return path.isAbsolute() ? userGameDirectories() : localGameDirectories();
}
/// Returns the store currently containing the game directory.
private static @Nullable GameDirectories findGameDirectoryStore(GameDirectory gameDirectory) {
Objects.requireNonNull(gameDirectory);
if (localGameDirectories().getGameDirectories().contains(gameDirectory)) {
return localGameDirectories();
}
if (userGameDirectories().getGameDirectories().contains(gameDirectory)) {
return userGameDirectories();
}
return null;
}
/// Returns the store currently containing the game directory, or throws when it is detached.
private static GameDirectories requireGameDirectoryStore(GameDirectory gameDirectory) {
@Nullable GameDirectories gameDirectories = findGameDirectoryStore(gameDirectory);
if (gameDirectories == null) {
throw new IllegalArgumentException("Game directory does not belong to a game directory store");
}
return gameDirectories;
}
/// Returns whether the given game directory store is read-only.
private static boolean isGameDirectoriesReadOnly(GameDirectories gameDirectories) {
return gameDirectories.isUserFile()
? SettingsManager.isUserGameDirectoriesReadOnly()
: SettingsManager.isLocalGameDirectoriesReadOnly();
}
/// Backs up and overwrites the given game directory store when it is read-only.
private static void forceOverwriteGameDirectoriesIfReadOnly(GameDirectories gameDirectories) {
if (!isGameDirectoriesReadOnly(gameDirectories)) {
return;
}
if (gameDirectories.isUserFile()) {
SettingsManager.forceOverwriteUserGameDirectories();
} else {
SettingsManager.forceOverwriteLocalGameDirectories();
}
}
/// Returns the selected game directory, creating built-in game directories first if the list is empty.
public static GameDirectory getSelectedGameDirectory() {
GameDirectory gameDirectory = selectedGameDirectory.get();
if (gameDirectory == null) {
throw new IllegalStateException("Selected game directory cannot be null");
}
return gameDirectory;
}
/// Returns the selected game repository.
public static HMCLGameRepository getSelectedRepository() {
HMCLGameRepository repository = selectedRepository.get();
if (repository == null) {
throw new IllegalStateException("Selected repository cannot be null");
}
return repository;
}
/// Sets the selected game directory.
public static void setSelectedGameDirectory(GameDirectory gameDirectory) {
Objects.requireNonNull(gameDirectory);
if (!mergedGameDirectories.contains(gameDirectory)) {
throw new IllegalArgumentException("Unknown game directory: " + gameDirectory);
}
selectedGameDirectory.set(gameDirectory);
}
/// Returns the selected game directory property.
///
/// The property is exposed for UI binding. Values should be changed through
/// [#setSelectedGameDirectory(GameDirectory)] or by bidirectional bindings that only select
/// game directories from [#getGameDirectories()].
public static ObjectProperty<GameDirectory> selectedGameDirectoryProperty() {
return selectedGameDirectory;
}
/// Returns the selected game repository property.
public static ObjectProperty<HMCLGameRepository> selectedRepositoryProperty() {
return selectedRepository;
}
/// Returns the selected instance property projected from the selected repository.
public static ReadOnlyStringProperty selectedInstanceProperty() {
return selectedInstance.getReadOnlyProperty();
}
/// Returns the selected instance ID for the selected repository.
public static @Nullable String getSelectedInstance() {
return getSelectedRepository().getSelectedInstance();
}
/// Sets the selected instance ID for the selected repository.
public static void setSelectedInstance(@Nullable String instance) {
getSelectedRepository().setSelectedInstance(instance);
}
/// Listeners notified after the selected repository has loaded versions.
private static final List<Consumer<HMCLGameRepository>> versionsListeners = new ArrayList<>(4);
/// Registers a listener that is notified when the selected repository's versions are available.
public static void registerVersionsListener(Consumer<HMCLGameRepository> listener) {
HMCLGameRepository repository = getSelectedRepository();
if (repository.isLoaded())
listener.accept(repository);
versionsListeners.add(listener);
}
}
@@ -582,15 +582,15 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// The selected game directory ID.
///
/// This field is owned by [Profiles]. Code outside [Profiles] should not modify it directly.
/// This field is owned by [GameDirectoryManager]. Code outside [GameDirectoryManager] should not modify it directly.
@SerializedName(PROPERTY_SELECTED_GAME_DIRECTORY)
private final ObjectProperty<@Nullable GameDirectoryID> selectedGameDirectory =
new SimpleObjectProperty<>(this, PROPERTY_SELECTED_GAME_DIRECTORY);
/// Returns the selected game directory ID property.
///
/// This property is exposed for persistence and [Profiles] integration. Code outside [Profiles]
/// should use `Profiles.setSelectedProfile` instead of modifying this property directly.
/// This property is exposed for persistence and [GameDirectoryManager] integration. Code outside [GameDirectoryManager]
/// should use `GameDirectoryManager.setSelectedGameDirectory` instead of modifying this property directly.
public ObjectProperty<@Nullable GameDirectoryID> selectedGameDirectoryProperty() {
return selectedGameDirectory;
}
@@ -607,29 +607,28 @@ public final class LauncherSettings extends ObservableSetting implements JsonSch
/// Selected instance IDs keyed by game directory ID.
///
/// This field is owned by [Profiles]. Code outside [Profiles] should not modify it directly.
/// This field is owned by [GameDirectoryManager]. Code outside [GameDirectoryManager] should not modify it directly.
@SerializedName(PROPERTY_SELECTED_INSTANCE)
private final ObservableMap<GameDirectoryID, String> selectedInstance = FXCollections.observableHashMap();
/// Returns selected instance IDs keyed by game directory ID.
///
/// This map is exposed for persistence and migration code. Runtime code outside [Profiles] should
/// use `Profiles.getSelectedInstance` and `Profiles.setSelectedInstance` instead of mutating it.
/// The map stores persisted selected instance values by game directory ID.
public ObservableMap<GameDirectoryID, String> getSelectedInstance() {
return selectedInstance;
}
/// Returns the selected instance ID for the given game directory ID.
///
/// This method is intended for [Profiles].
@Nullable String getSelectedInstance(@Nullable GameDirectoryID gameDirectoryId) {
/// The value is loaded by the game repository for the matching game directory.
public @Nullable String getSelectedInstance(@Nullable GameDirectoryID gameDirectoryId) {
return gameDirectoryId != null ? selectedInstance.get(gameDirectoryId) : null;
}
/// Sets the selected instance ID for the given game directory ID.
///
/// This method is intended for [Profiles].
void setSelectedInstance(@Nullable GameDirectoryID gameDirectoryId, @Nullable String selectedInstance) {
/// Blank values remove the persisted selected instance entry.
public void setSelectedInstance(@Nullable GameDirectoryID gameDirectoryId, @Nullable String selectedInstance) {
if (gameDirectoryId == null) {
return;
}
@@ -1245,7 +1245,7 @@ public final class LegacyConfigMigrator {
return changed;
}
/// Migrates profile-global game settings from config files used before HMCL 3.16.
/// Migrates global game settings stored under legacy profile entries before HMCL 3.16.
static void migrateLegacyPresetSettings(
GameDirectories gameDirectories,
GameSettingsPresets gameSettingsPresets,
@@ -1254,15 +1254,15 @@ public final class LegacyConfigMigrator {
return;
}
for (Profile profile : gameDirectories.getGameDirectories()) {
@Nullable GameSettingsPresetID legacyGameSettings = profile.getLegacyGameSettings();
for (GameDirectory gameDirectory : gameDirectories.getGameDirectories()) {
@Nullable GameSettingsPresetID legacyGameSettings = gameDirectory.getLegacyGameSettings();
if (legacyGameSettings == null) {
continue;
}
GameSettings.Preset legacyParent = gameSettingsPresets.getPreset(legacyGameSettings);
if (legacyParent == null) {
@Nullable String profileName = getLegacyProfileName(profile);
@Nullable String profileName = getLegacyProfileName(gameDirectory);
if (profileName == null) {
continue;
}
@@ -1279,14 +1279,14 @@ public final class LegacyConfigMigrator {
}
/// Returns the legacy profile name used in `configurations`.
private static @Nullable String getLegacyProfileName(Profile profile) {
if (LEGACY_DEFAULT_PROFILE_ID.equals(profile.getId())) {
private static @Nullable String getLegacyProfileName(GameDirectory gameDirectory) {
if (LEGACY_DEFAULT_PROFILE_ID.equals(gameDirectory.getId())) {
return LEGACY_DEFAULT_PROFILE;
}
if (LEGACY_HOME_PROFILE_ID.equals(profile.getId())) {
if (LEGACY_HOME_PROFILE_ID.equals(gameDirectory.getId())) {
return LEGACY_HOME_PROFILE;
}
return Profiles.getProfileCustomName(profile);
return GameDirectoryManager.getGameDirectoryCustomName(gameDirectory);
}
/// Detached settings migrated out of an old config file.
@@ -1,477 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 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.setting;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.util.PortablePath;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import org.jetbrains.annotations.UnmodifiableView;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Consumer;
import static org.jackhuang.hmcl.setting.SettingsManager.*;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/// Manages the merged runtime view of local and user game directory profiles.
///
/// Local profiles take precedence over user profiles with the same ID. This class is
/// the only owner of the currently selected profile and selected instance bookkeeping;
/// callers should use this API instead of mutating the profile-related fields in
/// [LauncherSettings] directly.
@NotNullByDefault
public final class Profiles {
/// The default current-workspace game directory path.
private static final PortablePath CURRENT_PROFILE_PATH = PortablePath.of(".minecraft");
/// The default user-home game directory path.
private static final PortablePath HOME_PROFILE_PATH = PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY);
private Profiles() {
}
/// Creates a profile ID that does not collide with existing profiles.
public static GameDirectoryID newProfileId() {
GameDirectoryID id;
do {
id = GameDirectoryID.generate();
} while (hasProfileId(id));
return id;
}
/// Returns whether an existing profile uses the given ID.
private static boolean hasProfileId(GameDirectoryID id) {
return localGameDirectories().getGameDirectories().stream()
.anyMatch(profile -> profile.getId().equals(id))
|| userGameDirectories().getGameDirectories().stream()
.anyMatch(profile -> profile.getId().equals(id));
}
/// Returns the display name for the given profile.
public static String getProfileDisplayName(Profile profile) {
String name = getProfileCustomName(profile);
if (name != null) {
return name;
}
if (isProfilePath(profile, CURRENT_PROFILE_PATH)) {
return i18n("profile.default");
}
if (isProfilePath(profile, HOME_PROFILE_PATH)) {
return i18n("profile.home");
}
return profile.getId().toString();
}
/// Returns the custom profile name in the current locale.
public static @Nullable String getProfileCustomName(Profile profile) {
@Nullable LocalizedText name = profile.getName();
return name != null ? name.getText(I18n.getLocale().getCandidateLocales()) : null;
}
/// Returns whether the profile uses the given path.
private static boolean isProfilePath(Profile profile, PortablePath expectedPath) {
PortablePath actualPath = profile.getPath();
return actualPath.isAbsolute() == expectedPath.isAbsolute()
&& actualPath.getPath().equals(expectedPath.getPath());
}
/// True if [#init()] hasn't been called.
private static boolean initialized = false;
private static final ObservableList<Profile> mergedProfiles =
FXCollections.observableArrayList(profile -> new javafx.beans.Observable[]{profile});
private static final @UnmodifiableView ObservableList<Profile> mergedProfilesUnmodifiable =
FXCollections.unmodifiableObservableList(mergedProfiles);
/// The selected profile, or `null` before the fallback profile is resolved.
private static final ObjectProperty<@UnknownNullability Profile> selectedProfile = new SimpleObjectProperty<>(Profiles.class, "selectedProfile");
private static final ReadOnlyStringWrapper selectedInstance = new ReadOnlyStringWrapper(Profiles.class, "selectedInstance");
/// Initializes profile state from the game directory stores loaded by [SettingsManager].
///
/// This method creates the built-in local and user-home profiles when required, rebuilds
/// the merged profile view, and restores the selected profile from [LauncherSettings].
public static void init() {
if (initialized)
throw new IllegalStateException("Already initialized");
initialized = true;
rebuildProfiles();
boolean needRebuildProfiles = false;
if (localGameDirectories().isNewlyCreated() && localGameDirectories().getGameDirectories().isEmpty()) {
needRebuildProfiles = true;
GameDirectories gameDirectories = localGameDirectories();
gameDirectories.getGameDirectories().add(new Profile(newProfileId(), null, CURRENT_PROFILE_PATH));
}
if (userGameDirectories().isNewlyCreated() && userGameDirectories().getGameDirectories().isEmpty()) {
needRebuildProfiles = true;
GameDirectories gameDirectories = userGameDirectories();
gameDirectories.getGameDirectories().add(new Profile(newProfileId(), null, HOME_PROFILE_PATH));
}
needRebuildProfiles |= createDefaultProfilesIfEmpty();
if (needRebuildProfiles) {
rebuildProfiles();
}
assert !mergedProfiles.isEmpty();
@Nullable GameDirectoryID selectedId = settings().selectedGameDirectoryProperty().get();
Profile currentProfile = null;
if (selectedId != null) {
for (Profile profile : mergedProfiles) {
if (profile.getId().equals(selectedId)) {
currentProfile = profile;
break;
}
}
}
selectedProfile.addListener((a, b, newValue) -> {
if (newValue == null) {
throw new IllegalStateException("selectedProfile cannot be null");
}
settings().selectedGameDirectoryProperty().set(newValue.getId());
selectedInstance.set(settings().getSelectedInstance(newValue.getId()));
newValue.getRepository().refreshVersionsAsync().start();
});
selectedProfile.set(currentProfile != null ? currentProfile : mergedProfiles.get(0));
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> {
runInFX(() -> {
@Nullable Profile profile = selectedProfile.get();
if (profile != null && profile.getRepository() == event.getSource()) {
refreshSelectedVersion(profile);
for (Consumer<Profile> listener : versionsListeners)
listener.accept(profile);
}
});
});
}
private static void refreshSelectedVersion(Profile profile) {
String version = settings().getSelectedInstance(profile.getId());
if (!profile.getRepository().hasVersion(version)) {
Optional<String> fallback = profile.getRepository().getVersions().stream()
.findFirst()
.map(Version::getId);
version = fallback.orElse(null);
if (!Objects.equals(settings().getSelectedInstance(profile.getId()), version)) {
settings().setSelectedInstance(profile.getId(), version);
}
}
selectedInstance.set(version);
}
/// Creates the built-in game directories only when no profile exists.
private static boolean createDefaultProfilesIfEmpty() {
if (localGameDirectories().getGameDirectories().isEmpty()
&& userGameDirectories().getGameDirectories().isEmpty()) {
localGameDirectories().getGameDirectories()
.add(new Profile(newProfileId(), null, CURRENT_PROFILE_PATH));
userGameDirectories().getGameDirectories()
.add(new Profile(newProfileId(), null, HOME_PROFILE_PATH));
return true;
} else {
return false;
}
}
/// Rebuilds the merged runtime profile view from the two backing stores.
private static void rebuildProfiles() {
GameDirectories userGameDirectories = userGameDirectories();
GameDirectories localGameDirectories = localGameDirectories();
Map<GameDirectoryID, Profile> visibleProfiles = new LinkedHashMap<>();
for (Profile profile : localGameDirectories.getGameDirectories()) {
GameDirectoryID id = profile.getId();
visibleProfiles.put(id, profile);
}
for (Profile profile : userGameDirectories.getGameDirectories()) {
GameDirectoryID id = profile.getId();
visibleProfiles.putIfAbsent(id, profile);
}
mergedProfiles.setAll(visibleProfiles.values());
}
/// Returns the read-only merged profile list.
public static @UnmodifiableView ObservableList<Profile> getProfiles() {
return mergedProfilesUnmodifiable;
}
/// Adds a profile to the per-workspace game directory store.
public static void addLocalProfile(Profile profile) {
if (SettingsManager.isLocalGameDirectoriesReadOnly()) {
throw new IllegalStateException("Local game directories are read-only");
}
addProfile(localGameDirectories(), profile);
}
/// Adds a profile to the user game directory store.
public static void addUserProfile(Profile profile) {
if (SettingsManager.isUserGameDirectoriesReadOnly()) {
throw new IllegalStateException("User game directories are read-only");
}
addProfile(userGameDirectories(), profile);
}
/// Returns whether an existing profile can be updated to the given path.
///
/// @param profile the profile to update
/// @param path the new profile path
/// @return whether both the current and target stores may be edited
public static boolean canUpdateProfile(Profile profile, PortablePath path) {
GameDirectories source = requireProfileStore(profile);
GameDirectories target = getProfileStore(path);
return !isGameDirectoriesReadOnly(source) && !isGameDirectoriesReadOnly(target);
}
/// Backs up and overwrites read-only game directory files required to update a profile.
///
/// @param profile the profile to update
/// @param path the new profile path
public static void forceOverwriteProfileFiles(Profile profile, PortablePath path) {
GameDirectories source = requireProfileStore(profile);
GameDirectories target = getProfileStore(path);
forceOverwriteGameDirectoriesIfReadOnly(source);
if (target != source) {
forceOverwriteGameDirectoriesIfReadOnly(target);
}
}
/// Updates a profile, moving it between local and user stores when the path type changes.
///
/// @param profile the profile to update
/// @param name the new custom profile name, or `null` for an unnamed profile
/// @param path the new profile path
public static void updateProfile(Profile profile, @Nullable LocalizedText name, PortablePath path) {
GameDirectories source = requireProfileStore(profile);
GameDirectories target = getProfileStore(path);
if (isGameDirectoriesReadOnly(source) || isGameDirectoriesReadOnly(target)) {
throw new IllegalStateException("Game directories are read-only");
}
if (source == target) {
profile.setName(name);
profile.setPath(path);
return;
}
source.getGameDirectories().remove(profile);
profile.setName(name);
profile.setPath(path);
addProfile(target, profile);
}
/// Returns whether the profile can be removed from all stores containing it.
///
/// @param profile the profile to remove
/// @return whether all containing stores may be edited
public static boolean canRemoveProfile(Profile profile) {
boolean localContains = localGameDirectories().getGameDirectories().contains(profile);
boolean userContains = userGameDirectories().getGameDirectories().contains(profile);
return (!localContains || !SettingsManager.isLocalGameDirectoriesReadOnly())
&& (!userContains || !SettingsManager.isUserGameDirectoriesReadOnly());
}
/// Backs up and overwrites read-only game directory files required to remove a profile.
///
/// @param profile the profile to remove
public static void forceOverwriteProfileFiles(Profile profile) {
if (localGameDirectories().getGameDirectories().contains(profile)) {
forceOverwriteGameDirectoriesIfReadOnly(localGameDirectories());
}
if (userGameDirectories().getGameDirectories().contains(profile)) {
forceOverwriteGameDirectoriesIfReadOnly(userGameDirectories());
}
}
/// Adds a profile to the given game directory store, replacing a profile with the same ID in that store.
private static void addProfile(GameDirectories gameDirectories, Profile profile) {
Objects.requireNonNull(profile);
ObservableList<Profile> profiles = gameDirectories.getGameDirectories();
GameDirectoryID id = profile.getId();
for (int i = 0; i < profiles.size(); i++) {
if (profiles.get(i).getId().equals(id)) {
profiles.set(i, profile);
rebuildProfiles();
return;
}
}
profiles.add(profile);
rebuildProfiles();
}
/// Removes a profile and recreates the built-in game directories when the list becomes empty.
public static void removeProfile(Profile profile) {
if (!canRemoveProfile(profile)) {
throw new IllegalStateException("Game directories are read-only");
}
userGameDirectories().getGameDirectories().remove(profile);
localGameDirectories().getGameDirectories().remove(profile);
createDefaultProfilesIfEmpty();
rebuildProfiles();
if (!mergedProfiles.contains(selectedProfile.get())) {
setSelectedProfile(mergedProfiles.get(0));
}
}
/// Returns the store that should own profiles with the given path.
private static GameDirectories getProfileStore(PortablePath path) {
Objects.requireNonNull(path);
return path.isAbsolute() ? userGameDirectories() : localGameDirectories();
}
/// Returns the store currently containing the profile.
private static @Nullable GameDirectories findProfileStore(Profile profile) {
Objects.requireNonNull(profile);
if (localGameDirectories().getGameDirectories().contains(profile)) {
return localGameDirectories();
}
if (userGameDirectories().getGameDirectories().contains(profile)) {
return userGameDirectories();
}
return null;
}
/// Returns the store currently containing the profile, or throws when it is detached.
private static GameDirectories requireProfileStore(Profile profile) {
@Nullable GameDirectories gameDirectories = findProfileStore(profile);
if (gameDirectories == null) {
throw new IllegalArgumentException("Profile does not belong to a game directory store");
}
return gameDirectories;
}
/// Returns whether the given game directory store is read-only.
private static boolean isGameDirectoriesReadOnly(GameDirectories gameDirectories) {
return gameDirectories.isUserFile()
? SettingsManager.isUserGameDirectoriesReadOnly()
: SettingsManager.isLocalGameDirectoriesReadOnly();
}
/// Backs up and overwrites the given game directory store when it is read-only.
private static void forceOverwriteGameDirectoriesIfReadOnly(GameDirectories gameDirectories) {
if (!isGameDirectoriesReadOnly(gameDirectories)) {
return;
}
if (gameDirectories.isUserFile()) {
SettingsManager.forceOverwriteUserGameDirectories();
} else {
SettingsManager.forceOverwriteLocalGameDirectories();
}
}
/// Returns the selected profile, creating built-in profiles first if the profile list is empty.
public static Profile getSelectedProfile() {
Profile profile = selectedProfile.get();
if (profile == null) {
throw new IllegalStateException("Selected profile cannot be null");
}
return profile;
}
/// Sets the selected profile.
public static void setSelectedProfile(Profile profile) {
Objects.requireNonNull(profile);
if (!mergedProfiles.contains(profile)) {
throw new IllegalArgumentException("Unknown profile: " + profile);
}
selectedProfile.set(profile);
}
/// Returns the selected profile property.
///
/// The property is exposed for UI binding. Values should be changed through
/// [#setSelectedProfile(Profile)] or by bidirectional bindings that only select
/// profiles from [#getProfiles()].
public static ObjectProperty<Profile> selectedProfileProperty() {
return selectedProfile;
}
/// Returns the selected instance property for the selected profile.
public static ReadOnlyStringProperty selectedInstanceProperty() {
return selectedInstance.getReadOnlyProperty();
}
/// Returns the selected instance ID for the selected profile.
public static @Nullable String getSelectedInstance() {
return selectedInstance.get();
}
/// Returns the selected instance ID for the given profile.
public static @Nullable String getSelectedInstance(Profile profile) {
return settings().getSelectedInstance(profile.getId());
}
/// Sets the selected instance ID for the currently selected profile.
public static void setSelectedInstance(@Nullable String instance) {
@Nullable Profile profile = selectedProfile.get();
if (profile != null) {
setSelectedInstance(profile, instance);
}
}
/// Sets the selected instance ID for the given profile.
public static void setSelectedInstance(Profile profile, @Nullable String instance) {
settings().setSelectedInstance(profile.getId(), instance);
if (profile == selectedProfile.get()) {
selectedInstance.set(settings().getSelectedInstance(profile.getId()));
}
}
private static final List<Consumer<Profile>> versionsListeners = new ArrayList<>(4);
/// Registers a listener that is notified when the selected profile's versions are available.
public static void registerVersionsListener(Consumer<Profile> listener) {
Profile profile = getSelectedProfile();
if (profile.getRepository().isLoaded())
listener.accept(profile);
versionsListeners.add(listener);
}
}
@@ -1011,7 +1011,7 @@ public final class SettingsManager {
gameSettingsAccess = SettingFileAccess.READ_WRITE;
}
/// Backs up and overwrites local `config/game-directories.json` with the currently loaded profiles.
/// Backs up and overwrites local `config/game-directories.json` with the currently loaded game directories.
public static void forceOverwriteLocalGameDirectories() {
boolean installAutoSave = !localGameDirectories().isSavable();
LOCAL_GAME_DIRECTORIES_FILE.backupAndOverwrite(localGameDirectories());
@@ -1021,7 +1021,7 @@ public final class SettingsManager {
localGameDirectoriesAccess = SettingFileAccess.READ_WRITE;
}
/// Backs up and overwrites user `config/user-game-directories.json` with the currently loaded profiles.
/// Backs up and overwrites user `config/user-game-directories.json` with the currently loaded game directories.
public static void forceOverwriteUserGameDirectories() {
boolean installAutoSave = !userGameDirectories().isSavable();
USER_GAME_DIRECTORIES_FILE.backupAndOverwrite(userGameDirectories());
@@ -702,8 +702,8 @@ public final class Controllers {
Controllers.navigate(Controllers.getSettingsPage());
break;
case "hmcl://game/launch":
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, Profiles.getSelectedInstance(profile), LauncherHelper::setKeep);
var repository = GameDirectoryManager.getSelectedRepository();
Versions.launch(repository, repository.getSelectedInstance(), LauncherHelper::setKeep);
break;
}
} else {
@@ -0,0 +1,72 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 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.ui.directory;
import javafx.beans.binding.Bindings;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.ui.Controllers;
import org.jetbrains.annotations.NotNullByDefault;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/// Navigation drawer item for one game directory entry.
@NotNullByDefault
public class GameDirectoryListItem extends RadioButton {
/// Game directory represented by this item.
private final GameDirectory gameDirectory;
/// Creates a list item for the given game directory.
///
/// @param gameDirectory the represented game directory
public GameDirectoryListItem(GameDirectory gameDirectory) {
this.gameDirectory = gameDirectory;
getStyleClass().setAll("game-directory-list-item", "navigation-drawer-item");
setUserData(gameDirectory);
this.selectedProperty().bind(Bindings.equal(gameDirectory, GameDirectoryManager.selectedGameDirectoryProperty()));
}
/// Creates the JavaFX skin for this item.
@Override
protected Skin<?> createDefaultSkin() {
return new GameDirectoryListItemSkin(this);
}
/// Removes the represented game directory after handling read-only storage.
public void remove() {
if (!GameDirectoryManager.canRemoveGameDirectory(gameDirectory)) {
Controllers.confirmBackupAndOverwrite(i18n("settings.game_directories.read_only"), () -> {
GameDirectoryManager.forceOverwriteGameDirectoryFiles(gameDirectory);
GameDirectoryManager.removeGameDirectory(gameDirectory);
});
return;
}
GameDirectoryManager.removeGameDirectory(gameDirectory);
}
/// Returns the represented game directory.
public GameDirectory getGameDirectory() {
return gameDirectory;
}
}
@@ -15,7 +15,7 @@
* 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.ui.profile;
package org.jackhuang.hmcl.ui.directory;
import com.jfoenix.controls.JFXButton;
import javafx.css.PseudoClass;
@@ -25,16 +25,23 @@ import javafx.scene.Node;
import javafx.scene.control.SkinBase;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jetbrains.annotations.NotNullByDefault;
public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
/// Skin for [GameDirectoryListItem].
@NotNullByDefault
public class GameDirectoryListItemSkin extends SkinBase<GameDirectoryListItem> {
/// Pseudo class applied to the selected item.
private static final PseudoClass SELECTED = PseudoClass.getPseudoClass("selected");
public ProfileListItemSkin(ProfileListItem skinnable) {
/// Creates the skin for a game directory list item.
///
/// @param skinnable the list item controlled by this skin
public GameDirectoryListItemSkin(GameDirectoryListItem skinnable) {
super(skinnable);
BorderPane root = new BorderPane();
@@ -45,7 +52,7 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
skinnable.pseudoClassStateChanged(SELECTED, active);
});
FXUtils.onClicked(getSkinnable(), () -> Profiles.setSelectedProfile(skinnable.getProfile()));
FXUtils.onClicked(getSkinnable(), () -> GameDirectoryManager.setSelectedGameDirectory(skinnable.getGameDirectory()));
Node left = SVG.FOLDER.createIcon(20);
left.setMouseTransparent(true);
@@ -67,8 +74,8 @@ public class ProfileListItemSkin extends SkinBase<ProfileListItem> {
right.getChildren().add(btnRemove);
root.setRight(right);
item.titleProperty().bind(skinnable.titleProperty());
item.subtitleProperty().bind(skinnable.subtitleProperty());
item.titleProperty().set(GameDirectoryManager.getGameDirectoryDisplayName(skinnable.getGameDirectory()));
item.subtitleProperty().set(skinnable.getGameDirectory().getPath().toString());
getChildren().setAll(container);
}
@@ -15,7 +15,7 @@
* 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.ui.profile;
package org.jackhuang.hmcl.ui.directory;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXTextField;
@@ -33,8 +33,8 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -44,6 +44,7 @@ import org.jackhuang.hmcl.util.PortablePath;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.i18n.LocalizedText;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.NotNullByDefault;
import org.jetbrains.annotations.Nullable;
import java.nio.file.InvalidPathException;
@@ -53,26 +54,39 @@ import java.util.Optional;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ProfilePage extends BorderPane implements DecoratorPage {
/// Page used to create or edit a game directory entry.
@NotNullByDefault
public final class GameDirectoryPage extends BorderPane implements DecoratorPage {
/// Decorator title state for this page.
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
/// Editable game directory path shown in the directory chooser.
private final StringProperty location;
private final @Nullable Profile profile;
private final JFXTextField txtProfileName;
/// Game directory being edited, or `null` when creating a new entry.
private final @Nullable GameDirectory gameDirectory;
/// Text field used to edit the game directory display name.
private final JFXTextField txtGameDirectoryName;
/// Directory chooser bound to [#location].
private final LineFileChooserButton gameDir;
/// Toggle that controls whether a saved path is converted to a relative path when possible.
private final LineToggleButton toggleUseRelativePath;
/**
* @param profile null if creating a new profile.
*/
public ProfilePage(@Nullable Profile profile) {
/// Creates a page for adding a new game directory or editing an existing game directory.
///
/// @param gameDirectory the edited game directory, or `null` when creating a new entry
public GameDirectoryPage(@Nullable GameDirectory gameDirectory) {
getStyleClass().add("gray-background");
this.profile = profile;
String profileDisplayName = Optional.ofNullable(profile).map(Profiles::getProfileDisplayName).orElse("");
this.gameDirectory = gameDirectory;
String gameDirectoryDisplayName = Optional.ofNullable(gameDirectory).map(GameDirectoryManager::getGameDirectoryDisplayName).orElse("");
state.set(State.fromTitle(profile == null ? i18n("profile.new") : i18n("profile") + " - " + profileDisplayName));
state.set(State.fromTitle(gameDirectory == null ? i18n("game_directory.new") : i18n("game_directory") + " - " + gameDirectoryDisplayName));
location = new SimpleStringProperty(this, "location",
Optional.ofNullable(profile).map(Profile::getPath).map(PortablePath::toPath).map(FileUtils::getAbsolutePath).orElse(".minecraft"));
Optional.ofNullable(gameDirectory).map(GameDirectory::getPath).map(PortablePath::toPath).map(FileUtils::getAbsolutePath).orElse(".minecraft"));
ScrollPane scroll = new ScrollPane();
this.setCenter(scroll);
@@ -84,50 +98,50 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
{
ComponentList componentList = new ComponentList();
{
BorderPane profileNamePane = new BorderPane();
BorderPane gameDirectoryNamePane = new BorderPane();
{
Label label = new Label(i18n("profile.name"));
profileNamePane.setLeft(label);
Label label = new Label(i18n("game_directory.name"));
gameDirectoryNamePane.setLeft(label);
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
txtProfileName = new JFXTextField();
profileNamePane.setRight(txtProfileName);
txtGameDirectoryName = new JFXTextField();
gameDirectoryNamePane.setRight(txtGameDirectoryName);
RequiredFieldValidator validator = new RequiredFieldValidator();
validator.setMessage(i18n("input.not_empty"));
txtProfileName.getValidators().add(validator);
BorderPane.setMargin(txtProfileName, new Insets(8, 0, 8, 0));
txtGameDirectoryName.getValidators().add(validator);
BorderPane.setMargin(txtGameDirectoryName, new Insets(8, 0, 8, 0));
txtProfileName.setText(profileDisplayName);
txtProfileName.getValidators().add(new ValidatorBase() {
txtGameDirectoryName.setText(gameDirectoryDisplayName);
txtGameDirectoryName.getValidators().add(new ValidatorBase() {
{
setMessage(i18n("profile.already_exists"));
setMessage(i18n("game_directory.already_exists"));
}
@Override
protected void eval() {
JFXTextField control = (JFXTextField) this.getSrcControl();
hasErrors.set(Profiles.getProfiles().stream()
.anyMatch(profile -> Objects.equals(
Profiles.getProfileCustomName(profile), control.getText())));
hasErrors.set(GameDirectoryManager.getGameDirectories().stream()
.anyMatch(existingGameDirectory -> Objects.equals(
GameDirectoryManager.getGameDirectoryCustomName(existingGameDirectory), control.getText())));
}
});
}
gameDir = new LineFileChooserButton();
gameDir.setTitle(i18n("profile.instance_directory"));
gameDir.setFileChooserTitle(i18n("profile.instance_directory.choose"));
gameDir.setTitle(i18n("game_directory.instance_directory"));
gameDir.setFileChooserTitle(i18n("game_directory.instance_directory.choose"));
gameDir.setType(LineFileChooserButton.Type.OPEN_DIRECTORY);
gameDir.locationProperty().bindBidirectional(location);
toggleUseRelativePath = new LineToggleButton();
toggleUseRelativePath.setTitle(i18n("profile.use_relative_path"));
toggleUseRelativePath.setTitle(i18n("game_directory.use_relative_path"));
gameDir.convertToRelativePathProperty().bind(toggleUseRelativePath.selectedProperty());
if (profile != null) {
toggleUseRelativePath.setSelected(!profile.getPath().isAbsolute());
if (gameDirectory != null) {
toggleUseRelativePath.setSelected(!gameDirectory.getPath().isAbsolute());
}
componentList.getContent().setAll(profileNamePane, gameDir, toggleUseRelativePath);
componentList.getContent().setAll(gameDirectoryNamePane, gameDir, toggleUseRelativePath);
}
rootPane.getChildren().setAll(componentList);
@@ -149,8 +163,8 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
saveButton.setPrefSize(100, 40);
saveButton.setOnAction(e -> onSave());
saveButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> !txtProfileName.validate() || StringUtils.isBlank(getLocation()),
txtProfileName.textProperty(), location));
() -> !txtGameDirectoryName.validate() || StringUtils.isBlank(getLocation()),
txtGameDirectoryName.textProperty(), location));
}
ChangeListener<String> locationChangeListener = (observable, oldValue, newValue) -> {
@@ -170,64 +184,65 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
String suggestedName = FileUtils.getName(parent);
if (!suggestedName.isBlank()) {
txtProfileName.setText(suggestedName);
txtGameDirectoryName.setText(suggestedName);
}
};
locationProperty().addListener(locationChangeListener);
txtProfileName.textProperty().addListener(new ChangeListener<>() {
txtGameDirectoryName.textProperty().addListener(new ChangeListener<>() {
@Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
if (txtProfileName.isFocused()) {
txtProfileName.textProperty().removeListener(this);
if (txtGameDirectoryName.isFocused()) {
txtGameDirectoryName.textProperty().removeListener(this);
locationProperty().removeListener(locationChangeListener);
}
}
});
}
/// Saves the edited game directory or adds a new entry to the appropriate game directory store.
private void onSave() {
if (profile != null) {
LocalizedText name = LocalizedText.plain(txtProfileName.getText());
PortablePath path = StringUtils.isNotBlank(getLocation()) ? createPortableLocation() : profile.getPath();
if (!Profiles.canUpdateProfile(profile, path)) {
if (gameDirectory != null) {
LocalizedText name = LocalizedText.plain(txtGameDirectoryName.getText());
PortablePath path = StringUtils.isNotBlank(getLocation()) ? createPortableLocation() : gameDirectory.getPath();
if (!GameDirectoryManager.canUpdateGameDirectory(gameDirectory, path)) {
Controllers.confirmBackupAndOverwrite(i18n("settings.game_directories.read_only"), () -> {
Profiles.forceOverwriteProfileFiles(profile, path);
Profiles.updateProfile(profile, name, path);
GameDirectoryManager.forceOverwriteGameDirectoryFiles(gameDirectory, path);
GameDirectoryManager.updateGameDirectory(gameDirectory, name, path);
fireEvent(new PageCloseEvent());
});
return;
}
Profiles.updateProfile(profile, name, path);
GameDirectoryManager.updateGameDirectory(gameDirectory, name, path);
} else {
if (StringUtils.isBlank(getLocation())) {
gameDir.fire();
}
Profile newProfile = new Profile(
Profiles.newProfileId(),
LocalizedText.plain(txtProfileName.getText()),
GameDirectory newGameDirectory = new GameDirectory(
GameDirectoryManager.newGameDirectoryId(),
LocalizedText.plain(txtGameDirectoryName.getText()),
createPortableLocation());
if (newProfile.getPath().isAbsolute()) {
if (newGameDirectory.getPath().isAbsolute()) {
if (SettingsManager.isUserGameDirectoriesReadOnly()) {
Controllers.confirmBackupAndOverwrite(i18n("settings.game_directories.read_only"), () -> {
SettingsManager.forceOverwriteUserGameDirectories();
Profiles.addUserProfile(newProfile);
GameDirectoryManager.addUserGameDirectory(newGameDirectory);
fireEvent(new PageCloseEvent());
});
return;
}
Profiles.addUserProfile(newProfile);
GameDirectoryManager.addUserGameDirectory(newGameDirectory);
} else {
if (SettingsManager.isLocalGameDirectoriesReadOnly()) {
Controllers.confirmBackupAndOverwrite(i18n("settings.game_directories.read_only"), () -> {
SettingsManager.forceOverwriteLocalGameDirectories();
Profiles.addLocalProfile(newProfile);
GameDirectoryManager.addLocalGameDirectory(newGameDirectory);
fireEvent(new PageCloseEvent());
});
return;
}
Profiles.addLocalProfile(newProfile);
GameDirectoryManager.addLocalGameDirectory(newGameDirectory);
}
}
@@ -249,19 +264,25 @@ public final class ProfilePage extends BorderPane implements DecoratorPage {
return PortablePath.of(getLocation());
}
/// Returns the decorator title state for this page.
@Override
public ReadOnlyObjectProperty<State> stateProperty() {
return state.getReadOnlyProperty();
}
/// Returns the current directory path text.
public String getLocation() {
return location.get();
}
/// Returns the editable directory path property.
public StringProperty locationProperty() {
return location;
}
/// Updates the directory path text.
///
/// @param location the new directory path text
public void setLocation(String location) {
this.location.set(location);
}
@@ -25,9 +25,9 @@ import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.download.game.GameRemoteVersion;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -69,11 +69,11 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage {
public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_MOD =
(downloadProvider, profile, version, mod, file) -> download(downloadProvider, profile, version, file, "mods");
(downloadProvider, repository, version, mod, file) -> download(downloadProvider, repository, version, file, "mods");
public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_RESOURCE_PACK =
(downloadProvider, profile, version, pack, file) -> download(downloadProvider, profile, version, file, "resourcepacks");
(downloadProvider, repository, version, pack, file) -> download(downloadProvider, repository, version, file, "resourcepacks");
public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_SHADER =
(downloadProvider, profile, version, shader, file) -> download(downloadProvider, profile, version, file, "shaderpacks");
(downloadProvider, repository, version, shader, file) -> download(downloadProvider, repository, version, file, "shaderpacks");
private final ReadOnlyObjectWrapper<DecoratorPage.State> state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1));
private final TabHeader tab;
@@ -96,8 +96,8 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
newGameTab.setNodeSupplier(loadVersionFor(() -> new VersionsPage(versionPageNavigator, i18n("install.installer.choose", i18n("install.installer.game")), "", DownloadProviders.getDownloadProvider(),
"game", versionPageNavigator::onGameSelected)));
modpackTab.setNodeSupplier(loadVersionFor(() -> {
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, profile, __, modpack, file) -> {
Versions.downloadModpackImpl(downloadProvider, profile, uploadVersion, modpack, file);
DownloadListPage page = HMCLLocalizedDownloadListPage.ofModPack((downloadProvider, repository, __, modpack, file) -> {
Versions.downloadModpackImpl(downloadProvider, repository, uploadVersion, modpack, file);
}, false);
JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack"));
@@ -112,7 +112,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
worldTab.setNodeSupplier(loadVersionFor(() -> new DownloadListPage(CurseForgeRemoteAddonRepository.WORLDS)));
tab = new TabHeader(transitionPane, newGameTab, modpackTab, modTab, resourcePackTab, shaderTab, worldTab);
Profiles.registerVersionsListener(this::loadVersions);
GameDirectoryManager.registerVersionsListener(this::loadVersions);
tab.select(newGameTab);
@@ -134,17 +134,17 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
private static <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
return () -> {
T node = nodeSupplier.get();
if (node instanceof VersionPage.VersionLoadable) {
((VersionPage.VersionLoadable) node).loadVersion(Profiles.getSelectedProfile(), null);
if (node instanceof VersionPage.GameInstanceLoadable loadable) {
loadable.loadInstance(GameDirectoryManager.getSelectedRepository(), null);
}
return node;
};
}
public static void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteAddon.Version file, String subdirectoryName) {
if (version == null) version = Profiles.getSelectedInstance(profile);
public static void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable String version, RemoteAddon.Version file, String subdirectoryName) {
if (version == null) version = repository.getSelectedInstance();
Path runDirectory = profile.getRepository().hasVersion(version) ? profile.getRepository().getRunDirectory(version) : profile.getRepository().getBaseDirectory();
Path runDirectory = repository.hasVersion(version) ? repository.getRunDirectory(version) : repository.getBaseDirectory();
Set<String> existingFiles;
@@ -178,29 +178,29 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}
}), i18n("message.downloading"), TaskCancellationAction.NORMAL);
handler.resolve();
}, file.file().filename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("profile.already_exists"), (it) -> !finalExistingFiles.contains(it)));
}, file.file().filename(), new Validator(i18n("install.new_game.malformed"), FileUtils::isNameValid), new Validator(i18n("game_directory.already_exists"), (it) -> !finalExistingFiles.contains(it)));
}
private void loadVersions(Profile profile) {
private void loadVersions(HMCLGameRepository repository) {
listenerHolder = new WeakListenerHolder();
runInFX(() -> {
if (profile == Profiles.getSelectedProfile()) {
listenerHolder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedInstanceProperty(), version -> {
if (repository.getGameDirectory() == GameDirectoryManager.getSelectedGameDirectory()) {
listenerHolder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), version -> {
if (modTab.isInitialized()) {
modTab.getNode().loadVersion(profile, null);
modTab.getNode().loadInstance(repository, null);
}
if (modpackTab.isInitialized()) {
modpackTab.getNode().loadVersion(profile, null);
modpackTab.getNode().loadInstance(repository, null);
}
if (resourcePackTab.isInitialized()) {
resourcePackTab.getNode().loadVersion(profile, null);
resourcePackTab.getNode().loadInstance(repository, null);
}
if (shaderTab.isInitialized()) {
shaderTab.getNode().loadVersion(profile, null);
shaderTab.getNode().loadInstance(repository, null);
}
if (worldTab.isInitialized()) {
worldTab.getNode().loadVersion(profile, null);
worldTab.getNode().loadInstance(repository, null);
}
}));
}
@@ -277,30 +277,31 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
}
public void onGameSelected() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(profile, (GameRemoteVersion) settings.get("game")), i18n("install.new_game"));
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
if (repository.isLoaded()) {
Controllers.getDecorator().startWizard(new VanillaInstallWizardProvider(repository, (GameRemoteVersion) settings.get("game")), i18n("install.new_game"));
}
}
}
private static class VanillaInstallWizardProvider implements WizardProvider {
private final Profile profile;
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependencyManager;
private final DownloadProvider downloadProvider;
private final GameRemoteVersion gameVersion;
public VanillaInstallWizardProvider(Profile profile, GameRemoteVersion gameVersion) {
this.profile = profile;
public VanillaInstallWizardProvider(HMCLGameRepository repository, GameRemoteVersion gameVersion) {
this.repository = repository;
this.gameVersion = gameVersion;
this.downloadProvider = DownloadProviders.getDownloadProvider();
this.dependencyManager = profile.getDependency(downloadProvider);
this.dependencyManager = repository.getDependency(downloadProvider);
}
@Override
public void start(SettingsMap settings) {
settings.put(ModpackPage.PROFILE, profile);
settings.put(ModpackPage.GAME_DIRECTORY, repository.getGameDirectory());
settings.put(ModpackPage.REPOSITORY, repository);
settings.put(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId(), gameVersion);
}
@@ -318,9 +319,9 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
});
return builder.buildAsync().whenComplete(any -> {
profile.getRepository().refreshVersions();
profile.getRepository().applyDefaultIsolationSetting(name);
}).thenRunAsync(Schedulers.javafx(), () -> Profiles.setSelectedInstance(profile, name));
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
}).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@Override
@@ -336,7 +337,7 @@ public class DownloadPage extends DecoratorAnimatedPage implements DecoratorPage
public Node createPage(WizardController controller, int step, SettingsMap settings) {
switch (step) {
case 0:
return new InstallersPage(controller, profile.getRepository(), ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider);
return new InstallersPage(controller, repository, ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider);
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
@@ -25,8 +25,7 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.modpack.Modpack;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -56,7 +55,7 @@ public final class LocalModpackPage extends ModpackPage {
public LocalModpackPage(WizardController controller) {
super(controller);
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
HMCLGameRepository repository = controller.getSettings().get(ModpackPage.REPOSITORY);
String name = controller.getSettings().get(MODPACK_NAME);
if (name != null) {
@@ -67,15 +66,15 @@ public final class LocalModpackPage extends ModpackPage {
if (installAsVersion) {
txtModpackName.getValidators().setAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
} else {
txtModpackName.getValidators().setAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !ModpackHelper.isExternalGameNameConflicts(str)
&& Profiles.getProfiles().stream()
&& GameDirectoryManager.getGameDirectories().stream()
.noneMatch(existingProfile ->
str.equals(Profiles.getProfileCustomName(existingProfile)))),
str.equals(GameDirectoryManager.getGameDirectoryCustomName(existingProfile)))),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
}
});
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.download;
import javafx.scene.Node;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ManuallyCreatedModpackException;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException;
@@ -25,8 +26,6 @@ import org.jackhuang.hmcl.modpack.Modpack;
import org.jackhuang.hmcl.modpack.ModpackCompletionException;
import org.jackhuang.hmcl.modpack.UnsupportedModpackException;
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -44,26 +43,26 @@ import java.nio.file.Path;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class ModpackInstallWizardProvider implements WizardProvider {
private final Profile profile;
private final HMCLGameRepository repository;
private final Path file;
private final String updateVersion;
private String iconUrl;
private boolean hasSource;
public ModpackInstallWizardProvider(Profile profile) {
this(profile, null, null);
public ModpackInstallWizardProvider(HMCLGameRepository repository) {
this(repository, null, null);
}
public ModpackInstallWizardProvider(Profile profile, Path modpackFile) {
this(profile, modpackFile, null);
public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile) {
this(repository, modpackFile, null);
}
public ModpackInstallWizardProvider(Profile profile, String updateVersion) {
this(profile, null, updateVersion);
public ModpackInstallWizardProvider(HMCLGameRepository repository, String updateVersion) {
this(repository, null, updateVersion);
}
public ModpackInstallWizardProvider(Profile profile, Path modpackFile, String updateVersion) {
this.profile = profile;
public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile, String updateVersion) {
this.repository = repository;
this.file = modpackFile;
this.updateVersion = updateVersion;
}
@@ -80,7 +79,8 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
settings.put(LocalModpackPage.MODPACK_NAME, updateVersion);
if (StringUtils.isNotBlank(iconUrl))
settings.put(LocalModpackPage.MODPACK_ICON_URL, iconUrl);
settings.put(ModpackPage.PROFILE, profile);
settings.put(ModpackPage.GAME_DIRECTORY, repository.getGameDirectory());
settings.put(ModpackPage.REPOSITORY, repository);
hasSource = settings.containsKey(LocalModpackPage.MODPACK_FILE) || settings.containsKey(RemoteModpackPage.MODPACK_SERVER_MANIFEST);
}
@@ -94,7 +94,7 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false);
if (isManuallyCreated) {
return ModpackHelper.getInstallManuallyCreatedModpackTask(profile, selected, name, charset);
return ModpackHelper.getInstallManuallyCreatedModpackTask(selected, name, charset);
}
if ((selected == null && serverModpackManifest == null) || modpack == null || name == null) return null;
@@ -106,9 +106,9 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
}
try {
if (serverModpackManifest != null) {
return ModpackHelper.getUpdateTask(profile, serverModpackManifest, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(name)));
} else {
return ModpackHelper.getUpdateTask(profile, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(profile.getRepository().getModpackConfiguration(name)));
return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(name)));
}
} catch (UnsupportedModpackException | ManuallyCreatedModpackException e) {
Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR);
@@ -120,11 +120,11 @@ public final class ModpackInstallWizardProvider implements WizardProvider {
return null;
} else {
if (serverModpackManifest != null) {
return ModpackHelper.getInstallTask(profile, serverModpackManifest, name, modpack)
.thenRunAsync(Schedulers.javafx(), () -> Profiles.setSelectedInstance(profile, name));
return ModpackHelper.getInstallTask(repository, serverModpackManifest, name, modpack)
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
} else {
return ModpackHelper.getInstallTask(profile, selected, name, modpack, iconUrl)
.thenRunAsync(Schedulers.javafx(), () -> Profiles.setSelectedInstance(profile, name));
return ModpackHelper.getInstallTask(repository, selected, name, modpack, iconUrl)
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
}
}
@@ -23,7 +23,8 @@ import javafx.beans.property.StringProperty;
import javafx.geometry.Pos;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ComponentList;
import org.jackhuang.hmcl.ui.construct.LinePane;
@@ -37,7 +38,8 @@ import static javafx.beans.binding.Bindings.createBooleanBinding;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public abstract class ModpackPage extends SpinnerPane implements WizardPage {
public static final SettingsMap.Key<Profile> PROFILE = new SettingsMap.Key<>("PROFILE");
public static final SettingsMap.Key<GameDirectory> GAME_DIRECTORY = new SettingsMap.Key<>("GAME_DIRECTORY");
public static final SettingsMap.Key<HMCLGameRepository> REPOSITORY = new SettingsMap.Key<>("REPOSITORY");
protected final WizardController controller;
@@ -21,7 +21,6 @@ import javafx.application.Platform;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.modpack.Modpack;
import org.jackhuang.hmcl.modpack.server.ServerModpackManifest;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.WebPage;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
@@ -57,7 +56,7 @@ public final class RemoteModpackPage extends ModpackPage {
versionProperty.set(manifest.getVersion());
authorProperty.set(manifest.getAuthor());
Profile profile = controller.getSettings().get(ModpackPage.PROFILE);
HMCLGameRepository repository = controller.getSettings().get(ModpackPage.REPOSITORY);
String name = controller.getSettings().get(MODPACK_NAME);
if (name != null) {
txtModpackName.setText(name);
@@ -67,7 +66,7 @@ public final class RemoteModpackPage extends ModpackPage {
txtModpackName.setText(manifest.getName().trim());
txtModpackName.getValidators().addAll(
new RequiredValidator(),
new Validator(i18n("install.new_game.already_exists"), str -> !profile.getRepository().versionIdConflicts(str)),
new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)),
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId));
}
@@ -21,9 +21,9 @@ import javafx.scene.Node;
import org.jackhuang.hmcl.download.*;
import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask;
import org.jackhuang.hmcl.download.game.LibraryDownloadException;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.DownloadException;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -46,7 +46,7 @@ import java.util.zip.ZipException;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class UpdateInstallerWizardProvider implements WizardProvider {
private final Profile profile;
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependencyManager;
private final String gameVersion;
private final Version version;
@@ -54,14 +54,14 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
private final String oldLibraryVersion;
private final DownloadProvider downloadProvider;
public UpdateInstallerWizardProvider(@NotNull Profile profile, @NotNull String gameVersion, @NotNull Version version, @NotNull String libraryId, @Nullable String oldLibraryVersion) {
this.profile = profile;
public UpdateInstallerWizardProvider(@NotNull HMCLGameRepository repository, @NotNull String gameVersion, @NotNull Version version, @NotNull String libraryId, @Nullable String oldLibraryVersion) {
this.repository = repository;
this.gameVersion = gameVersion;
this.version = version;
this.libraryId = libraryId;
this.oldLibraryVersion = oldLibraryVersion;
this.downloadProvider = DownloadProviders.getDownloadProvider();
this.dependencyManager = profile.getDependency(downloadProvider);
this.dependencyManager = repository.getDependency(downloadProvider);
}
@Override
@@ -91,7 +91,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
}
}
return ret.thenComposeAsync(profile.getRepository()::saveAsync).thenComposeAsync(profile.getRepository().refreshVersionsAsync()).withStagesHints(hints);
return ret.thenComposeAsync(repository::saveAsync).thenComposeAsync(repository.refreshVersionsAsync()).withStagesHints(hints);
}
@Override
@@ -103,7 +103,7 @@ public final class UpdateInstallerWizardProvider implements WizardProvider {
controller.onFinish();
} else if ("game".equals(libraryId)) {
String newGameVersion = ((RemoteVersion) settings.get(libraryId)).getSelfVersion();
controller.onNext(new AdditionalInstallersPage(newGameVersion, version, controller, profile.getRepository(), downloadProvider));
controller.onNext(new AdditionalInstallersPage(newGameVersion, version, controller, repository, downloadProvider));
} else {
Controllers.confirm(i18n("install.change_version.confirm", i18n("install.installer." + libraryId), oldLibraryVersion, ((RemoteVersion) settings.get(libraryId)).getSelfVersion()),
i18n("install.change_version"), controller::onFinish, controller::onCancel);
@@ -22,9 +22,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.GameBuilder;
import org.jackhuang.hmcl.download.RemoteVersion;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -34,19 +33,20 @@ import org.jackhuang.hmcl.util.SettingsMap;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public final class VanillaInstallWizardProvider implements WizardProvider {
private final Profile profile;
private final HMCLGameRepository repository;
private final DefaultDependencyManager dependencyManager;
private final DownloadProvider downloadProvider;
public VanillaInstallWizardProvider(Profile profile) {
this.profile = profile;
public VanillaInstallWizardProvider(HMCLGameRepository repository) {
this.repository = repository;
this.downloadProvider = DownloadProviders.getDownloadProvider();
this.dependencyManager = profile.getDependency(downloadProvider);
this.dependencyManager = repository.getDependency(downloadProvider);
}
@Override
public void start(SettingsMap settings) {
settings.put(ModpackPage.PROFILE, profile);
settings.put(ModpackPage.GAME_DIRECTORY, repository.getGameDirectory());
settings.put(ModpackPage.REPOSITORY, repository);
}
private Task<Void> finishVersionDownloadingAsync(SettingsMap settings) {
@@ -62,10 +62,10 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
});
return builder.buildAsync().whenComplete(any -> {
profile.getRepository().refreshVersions();
profile.getRepository().applyDefaultIsolationSetting(name);
repository.refreshVersions();
repository.applyDefaultIsolationSetting(name);
})
.thenRunAsync(Schedulers.javafx(), () -> Profiles.setSelectedInstance(profile, name));
.thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name));
}
@Override
@@ -82,7 +82,7 @@ public final class VanillaInstallWizardProvider implements WizardProvider {
switch (step) {
case 0:
return new VersionsPage(controller, i18n("install.installer.choose", i18n("install.installer.game")), "", downloadProvider, "game",
() -> controller.onNext(new InstallersPage(controller, profile.getRepository(), ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider)));
() -> controller.onNext(new InstallersPage(controller, repository, ((RemoteVersion) controller.getSettings().get("game")).getGameVersion(), downloadProvider)));
default:
throw new IllegalStateException("error step " + step + ", settings: " + settings + ", pages: " + controller.getPages());
}
@@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui.export;
import javafx.scene.Node;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.modpack.ModAdviser;
import org.jackhuang.hmcl.modpack.ModpackExportInfo;
import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackExportTask;
@@ -45,11 +46,11 @@ import java.util.List;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
public final class ExportWizardProvider implements WizardProvider {
private final Profile profile;
private final HMCLGameRepository repository;
private final String version;
public ExportWizardProvider(Profile profile, String version) {
this.profile = profile;
public ExportWizardProvider(HMCLGameRepository repository, String version) {
this.repository = repository;
this.version = version;
}
@@ -163,7 +164,7 @@ public final class ExportWizardProvider implements WizardProvider {
@Override
public void execute() {
dependency = new McbbsModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
dependency = new McbbsModpackExportTask(repository, version, exportInfo, modpackFile);
}
@Override
@@ -183,8 +184,8 @@ public final class ExportWizardProvider implements WizardProvider {
@Override
public void execute() {
GameSettings.Effective setting = profile.getRepository().getEffectiveGameSettings(version);
dependency = new MultiMCModpackExportTask(profile.getRepository(), version, exportInfo.getWhitelist(),
GameSettings.Effective setting = repository.getEffectiveGameSettings(version);
dependency = new MultiMCModpackExportTask(repository, version, exportInfo.getWhitelist(),
new MultiMCInstanceConfiguration(
"OneSix",
exportInfo.getName() + "-" + exportInfo.getVersion(),
@@ -231,7 +232,7 @@ public final class ExportWizardProvider implements WizardProvider {
@Override
public void execute() {
dependency = new ServerModpackExportTask(profile.getRepository(), version, exportInfo, modpackFile);
dependency = new ServerModpackExportTask(repository, version, exportInfo, modpackFile);
}
@Override
@@ -252,7 +253,7 @@ public final class ExportWizardProvider implements WizardProvider {
@Override
public void execute() {
dependency = new ModrinthModpackExportTask(
profile.getRepository(),
repository,
version,
exportInfo,
modpackFile
@@ -270,8 +271,8 @@ public final class ExportWizardProvider implements WizardProvider {
public Node createPage(WizardController controller, int step, SettingsMap settings) {
return switch (step) {
case 0 -> new ModpackTypeSelectionPage(controller);
case 1 -> new ModpackInfoPage(controller, profile.getRepository(), version);
case 2 -> new ModpackFileSelectionPage(controller, profile, version, ModAdviser::suggestMod);
case 1 -> new ModpackInfoPage(controller, repository, version);
case 2 -> new ModpackFileSelectionPage(controller, repository, version, ModAdviser::suggestMod);
default -> throw new IllegalArgumentException("step");
};
}
@@ -27,8 +27,8 @@ import javafx.scene.control.Label;
import javafx.scene.control.TreeItem;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.modpack.ModAdviser;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.NoneMultipleSelectionModel;
import org.jackhuang.hmcl.ui.wizard.WizardController;
@@ -59,13 +59,13 @@ public final class ModpackFileSelectionPage extends BorderPane implements Wizard
private final ModAdviser adviser;
private final CheckBoxTreeItem<String> rootNode;
public ModpackFileSelectionPage(WizardController controller, Profile profile, String version, ModAdviser adviser) {
public ModpackFileSelectionPage(WizardController controller, HMCLGameRepository repository, String version, ModAdviser adviser) {
this.controller = controller;
this.version = version;
this.adviser = adviser;
JFXTreeView<String> treeView = new JFXTreeView<>();
rootNode = getTreeItem(profile.getRepository().getRunDirectory(version), "minecraft");
rootNode = getTreeItem(repository.getRunDirectory(version), "minecraft");
treeView.setRoot(rootNode);
treeView.setSelectionModel(new NoneMultipleSelectionModel<>());
onEscPressed(treeView, () -> controller.onPrev(true));
@@ -51,7 +51,6 @@ import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.versions.VersionIconDialog;
import org.jackhuang.hmcl.ui.versions.VersionPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.Holder;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.ServerAddress;
@@ -78,7 +77,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
/// @author Glavo
@NotNullByDefault
public final class GameSettingsPage<S extends GameSettings> extends StackPane
implements DecoratorPage, VersionPage.VersionLoadable, PageAware {
implements DecoratorPage, VersionPage.GameInstanceLoadable, PageAware {
private static final Object INHERIT_BUTTON_TOOLTIP_KEY = new Object();
private static final PseudoClass PSEUDO_OVERRIDDEN = PseudoClass.getPseudoClass("overridden");
@@ -90,8 +89,11 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private final ObjectProperty<State> state = new SimpleObjectProperty<>(this, "state", new State("", null, false, false, false));
private final WeakListenerHolder holder = new WeakListenerHolder();
/// The selected profile.
private @Nullable Profile profile;
/// The selected game directory.
private @Nullable GameDirectory gameDirectory;
/// The selected repository.
private @Nullable HMCLGameRepository repository;
/// The current instance ID.
private @Nullable String instanceId;
@@ -844,7 +846,7 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
});
}
/// Refreshes parent preset display text because the implicit parent depends on the current profile.
/// Refreshes parent preset display text because the implicit parent depends on the current game directory.
private void refreshInstanceParentSettingConverter(LineSelectButton<GameSettings.@Nullable Preset> button) {
button.setConverter(setting -> setting != null
? PresetManagementPane.getPresetDisplayName(setting)
@@ -853,19 +855,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// Returns the label for the implicit parent preset selected by a null instance parent.
private String getImplicitParentGameSettingsDisplayName() {
GameSettings.Preset legacyParent = getProfileLegacyGameSettings();
GameSettings.Preset legacyParent = getGameDirectoryLegacyGameSettings();
return legacyParent != null
? PresetManagementPane.getPresetDisplayName(legacyParent)
: i18n("settings.type.global.preset.default");
}
/// Returns the migrated profile-level parent preset, or null when this profile uses the default preset.
private @Nullable GameSettings.Preset getProfileLegacyGameSettings() {
if (profile == null || profile.getLegacyGameSettings() == null) {
/// Returns the migrated game-directory-level parent preset, or `null` when this game directory uses the default preset.
private @Nullable GameSettings.Preset getGameDirectoryLegacyGameSettings() {
if (gameDirectory == null || gameDirectory.getLegacyGameSettings() == null) {
return null;
}
return SettingsManager.getGameSettings(profile.getLegacyGameSettings());
return SettingsManager.getGameSettings(gameDirectory.getLegacyGameSettings());
}
/// Adds the title-line inheritance button for the Java selection sublist.
@@ -1778,16 +1780,16 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
private boolean isCurrentInstanceModpack() {
return profile != null && instanceId != null && profile.getRepository().isModpack(instanceId);
return repository != null && instanceId != null && repository.isModpack(instanceId);
}
/// Returns the current instance version root displayed for modpack running directories.
private String getCurrentInstanceVersionRoot() {
if (profile == null || instanceId == null) {
if (repository == null || instanceId == null) {
return "";
}
return profile.getRepository().getVersionRoot(instanceId).toString();
return repository.getVersionRoot(instanceId).toString();
}
/// Keeps a listener attached to the current instance's parent preset property.
@@ -2385,10 +2387,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
return getEffectiveParentGameSettings(instance);
}
/// Returns the runtime parent preset for an instance, including profile-level legacy migration fallback.
/// Returns the runtime parent preset for an instance, including the game directory's migrated preset fallback.
private GameSettings.Preset getEffectiveParentGameSettings(GameSettings.Instance instance) {
if (profile != null) {
return profile.getRepository().getParentGameSettings(instance);
if (repository != null) {
return repository.getParentGameSettings(instance);
}
return getExplicitParentGameSettings(instance);
@@ -2428,14 +2430,14 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
@SuppressWarnings("unchecked")
@Override
public void loadVersion(Profile profile, @Nullable String instanceId) {
this.profile = profile;
public void loadInstance(HMCLGameRepository repository, @Nullable String instanceId) {
this.gameDirectory = repository.getGameDirectory();
this.repository = repository;
this.instanceId = instanceId;
assert isPresetSetting == (instanceId == null);
if (instanceId != null) {
HMCLGameRepository repository = profile.getRepository();
@Nullable GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId);
this.currentSetting.set((S) setting);
setSettingsReadOnly(
@@ -2491,12 +2493,12 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
/// Backs up and overwrites the current instance's `instance-game-settings.json`.
private void forceOverwriteInstanceGameSettings() {
if (profile == null || instanceId == null) {
if (repository == null || instanceId == null) {
return;
}
Controllers.confirmBackupAndOverwrite(i18n("settings.game.instance_settings.unsupported"), () -> {
profile.getRepository().forceOverwriteInstanceGameSettings(instanceId);
repository.forceOverwriteInstanceGameSettings(instanceId);
setSettingsReadOnly(false, "");
});
}
@@ -2510,10 +2512,10 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
}
private void loadIcon() {
if (profile == null || instanceId == null)
if (repository == null || instanceId == null)
return;
iconPickerItem.setImage(profile.getRepository().getVersionIconImage(instanceId));
iconPickerItem.setImage(repository.getVersionIconImage(instanceId));
}
/// Refreshes Java selection controls and keeps inherited parent Java properties observed.
@@ -2577,13 +2579,12 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
private void initJavaSubtitle() {
S setting = currentSetting.get();
if (setting == null || profile == null)
if (setting == null || gameDirectory == null)
return;
initializeSelectedJava();
HMCLGameRepository repository = this.profile.getRepository();
JavaVersionType javaVersionType = setting.javaTypeProperty().getValue();
GameSettings.Effective effectiveSetting = this.instanceId != null ? repository.getEffectiveGameSettings(this.instanceId) : null;
GameSettings.Effective effectiveSetting = this.instanceId != null && repository != null ? repository.getEffectiveGameSettings(this.instanceId) : null;
JavaVersionType effectiveJavaVersionType = effectiveSetting != null ? effectiveSetting.getInheritable(GameSettings::javaTypeProperty) : javaVersionType;
boolean autoSelected = effectiveJavaVersionType == JavaVersionType.AUTO || effectiveJavaVersionType == JavaVersionType.VERSION;
@@ -2605,8 +2606,8 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
gameVersionNumber = GameVersionNumber.unknown();
version = null;
} else {
gameVersionNumber = GameVersionNumber.asGameVersion(repository.getGameVersion(this.instanceId));
version = repository.getResolvedVersion(this.instanceId);
gameVersionNumber = repository != null ? GameVersionNumber.asGameVersion(repository.getGameVersion(this.instanceId)) : GameVersionNumber.unknown();
version = repository != null ? repository.getResolvedVersion(this.instanceId) : null;
}
try {
@@ -2626,24 +2627,19 @@ public final class GameSettingsPage<S extends GameSettings> extends StackPane
javaSublist.setDescription("");
}
private void editSpecificSettings() {
if (profile != null)
Versions.modifyGameSettings(profile, Profiles.getSelectedInstance(profile));
}
private void onExploreIcon() {
if (profile == null || instanceId == null)
if (repository == null || instanceId == null)
return;
Controllers.dialog(new VersionIconDialog(profile, instanceId, this::loadIcon));
Controllers.dialog(new VersionIconDialog(repository, instanceId, this::loadIcon));
}
private void onDeleteIcon() {
if (profile == null || instanceId == null)
if (repository == null || instanceId == null)
return;
profile.getRepository().deleteIconFile(instanceId);
GameSettings.Instance localGameSettings = profile.getRepository().getInstanceGameSettingsOrCreate(instanceId);
repository.deleteIconFile(instanceId);
GameSettings.Instance localGameSettings = repository.getInstanceGameSettingsOrCreate(instanceId);
if (localGameSettings != null) {
localGameSettings.iconProperty().setValue(VersionIconType.DEFAULT);
}
@@ -19,9 +19,9 @@ package org.jackhuang.hmcl.ui.main;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
@@ -59,7 +59,7 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
tab = new TabHeader(transitionPane, gameTab, javaManagementTab, settingsTab, personalizationTab, downloadTab, helpTab, feedbackTab, aboutTab);
tab.select(gameTab);
addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> gameTab.getNode().loadVersion(Profiles.getSelectedProfile(), null));
addEventHandler(Navigator.NavigationEvent.NAVIGATED, event -> gameTab.getNode().loadInstance(GameDirectoryManager.getSelectedRepository(), null));
AdvancedListBox sideBar = new AdvancedListBox()
.addNavigationDrawerTab(tab, gameTab, i18n("settings.type.global.manage"), SVG.STADIA_CONTROLLER, SVG.STADIA_CONTROLLER_FILL)
@@ -88,8 +88,8 @@ public class LauncherSettingsPage extends DecoratorAnimatedPage implements Decor
tab.onPageHidden();
}
public void showGameSettings(Profile profile) {
gameTab.getNode().loadVersion(profile, null);
public void showGameSettings(HMCLGameRepository repository) {
gameTab.getNode().loadInstance(repository, null);
tab.select(gameTab, false);
}
@@ -44,10 +44,11 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.DefaultDependencyManager;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.download.VersionList;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.theme.Themes;
@@ -94,7 +95,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private final BooleanProperty showUpdateDialog = new SimpleBooleanProperty(this, "showUpdateDialog");
private final ObjectProperty<RemoteVersion> latestVersion = new SimpleObjectProperty<>(this, "latestVersion");
private final ObservableList<Version> versions = FXCollections.observableArrayList();
private Profile profile;
private HMCLGameRepository repository;
private TransitionPane announcementPane;
private final StackPane updatePane;
@@ -209,7 +210,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
FXUtils.onScroll(launchPane, versions, list -> {
String currentId = getCurrentGame();
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
}, it -> Profiles.setSelectedInstance(profile, it.getId()));
}, it -> repository.setSelectedInstance(it.getId()));
StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT);
{
@@ -259,7 +260,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
JFXPopup.PopupHPosition.RIGHT,
0,
-menuButton.getHeight(),
profile, versions
repository, versions
));
FXUtils.installFastTooltip(menuButton, i18n("version.switch"));
menuButton.setGraphic(SVG.ARROW_DROP_UP.createIcon(30));
@@ -315,8 +316,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
}
private void launch() {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, Profiles.getSelectedInstance(profile));
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
Versions.launch(repository, repository.getSelectedInstance());
}
private void launchNoGame() {
@@ -332,8 +333,8 @@ public final class MainPage extends StackPane implements DecoratorPage {
.findFirst()
.orElseThrow(() -> new IOException("No versions found")))
.thenComposeAsync(version -> {
Profile profile = Profiles.getSelectedProfile();
DefaultDependencyManager dependency = profile.getDependency();
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
DefaultDependencyManager dependency = repository.getDependency();
String gameVersion = gameVersionHolder.value = version.getGameVersion();
return dependency.gameBuilder()
@@ -341,10 +342,10 @@ public final class MainPage extends StackPane implements DecoratorPage {
.gameVersion(gameVersion)
.buildAsync();
})
.whenComplete(any -> profile.getRepository().refreshVersions())
.whenComplete(any -> GameDirectoryManager.getSelectedRepository().refreshVersions())
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
Profiles.setSelectedInstance(profile, gameVersionHolder.value);
GameDirectoryManager.getSelectedRepository().setSelectedInstance(gameVersionHolder.value);
launch();
} else if (exception instanceof CancellationException) {
Controllers.showToast(i18n("message.cancelled"));
@@ -376,8 +377,12 @@ public final class MainPage extends StackPane implements DecoratorPage {
return state;
}
public Profile getProfile() {
return profile;
public GameDirectory getGameDirectory() {
return repository.getGameDirectory();
}
public HMCLGameRepository getRepository() {
return repository;
}
public String getCurrentGame() {
@@ -432,9 +437,9 @@ public final class MainPage extends StackPane implements DecoratorPage {
this.latestVersion.set(latestVersion);
}
public void initVersions(Profile profile, List<Version> versions) {
public void initVersions(HMCLGameRepository repository, List<Version> versions) {
FXUtils.checkFxUserThread();
this.profile = profile;
this.repository = repository;
this.versions.setAll(versions);
}
}
@@ -27,8 +27,8 @@ import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.terracotta.TerracottaMetadata;
@@ -77,9 +77,9 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class)
.register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource()));
Profile profile = Profiles.getSelectedProfile();
if (profile != null && profile.getRepository().isLoaded())
onRefreshedVersions(Profiles.selectedProfileProperty().get().getRepository());
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
if (repository.isLoaded())
onRefreshedVersions(GameDirectoryManager.getSelectedRepository());
getStyleClass().remove("gray-background");
getLeft().getStyleClass().add("gray-background");
@@ -104,7 +104,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
Path file = modpacks.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
Controllers.getDecorator().startWizard(
new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file),
new ModpackInstallWizardProvider(GameDirectoryManager.getSelectedRepository(), file),
i18n("install.modpack"));
} else if (NBTFileType.isNBTFileByExtension(file)) {
try {
@@ -115,15 +115,15 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
i18n("message.error"), MessageDialogPane.MessageType.ERROR);
}
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
Versions.installFromJson(Profiles.getSelectedProfile(), file);
Versions.installFromJson(GameDirectoryManager.getSelectedRepository(), file);
}
});
FXUtils.onChangeAndOperate(Profiles.selectedInstanceProperty(), mainPage::setCurrentGame);
FXUtils.onChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), mainPage::setCurrentGame);
mainPage.latestVersionProperty().bind(UpdateChecker.latestVersionProperty());
Profiles.registerVersionsListener(profile -> {
HMCLGameRepository repository = profile.getRepository();
GameDirectoryManager.registerVersionsListener(repository -> {
GameDirectory gameDirectory = repository.getGameDirectory();
List<Version> children = repository.getVersions().parallelStream()
.filter(version -> !version.isHidden())
.sorted(Comparator
@@ -131,8 +131,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
.thenComparing(version -> VersionNumber.asVersion(repository.getGameVersion(version).orElse(version.getId()))))
.collect(Collectors.toList());
runInFX(() -> {
if (profile == Profiles.getSelectedProfile())
mainPage.initVersions(profile, children);
if (gameDirectory == GameDirectoryManager.getSelectedGameDirectory())
mainPage.initVersions(repository, children);
});
});
this.mainPage = mainPage;
@@ -154,18 +154,17 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
// second item in left sidebar
GameAdvancedListItem gameListItem = new GameAdvancedListItem();
gameListItem.setOnAction(e -> {
Profile profile = Profiles.getSelectedProfile();
String version = Profiles.getSelectedInstance();
String version = GameDirectoryManager.getSelectedRepository().getSelectedInstance();
if (version == null) {
Controllers.navigate(Controllers.getGameListPage());
} else {
Versions.modifyGameSettings(profile, version);
Versions.modifyGameSettings(GameDirectoryManager.getSelectedRepository(), version);
}
});
FXUtils.onScroll(gameListItem, getSkinnable().getMainPage().getVersions(), list -> {
String currentId = getSkinnable().getMainPage().getCurrentGame();
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
}, it -> Profiles.setSelectedInstance(getSkinnable().getMainPage().getProfile(), it.getId()));
}, it -> getSkinnable().getMainPage().getRepository().setSelectedInstance(it.getId()));
if (AnimationUtils.isAnimationEnabled()) {
FXUtils.prepareOnMouseEnter(gameListItem, Controllers::prepareVersionPage);
}
@@ -195,7 +194,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
launcherSettingsItem.setLeftIcon(SVG.SETTINGS);
launcherSettingsItem.setTitle(i18n("settings"));
launcherSettingsItem.setOnAction(e -> {
Controllers.getSettingsPage().showGameSettings(Profiles.getSelectedProfile());
Controllers.getSettingsPage().showGameSettings(GameDirectoryManager.getSelectedRepository());
Controllers.navigate(Controllers.getSettingsPage());
});
if (AnimationUtils.isAnimationEnabled()) {
@@ -252,7 +251,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
JFXPopup.PopupHPosition.LEFT,
gameListItem.getWidth(),
0,
getSkinnable().getMainPage().getProfile(),
getSkinnable().getMainPage().getRepository(),
getSkinnable().getMainPage().getVersions());
}
}
@@ -281,7 +280,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage {
Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile))
.thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding))
.thenApplyAsync(modpack -> ModpackHelper
.getInstallTask(repository.getProfile(), modpackFile, modpack.getName(), modpack, null)
.getInstallTask(repository, modpackFile, modpack.getName(), modpack, null)
.executor())
.thenAcceptAsync(Schedulers.javafx(), executor -> {
Controllers.taskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL);
@@ -1,92 +0,0 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> 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.ui.profile;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.control.RadioButton;
import javafx.scene.control.Skin;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.Controllers;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class ProfileListItem extends RadioButton {
private final Profile profile;
private final StringProperty title = new SimpleStringProperty();
private final StringProperty subtitle = new SimpleStringProperty();
public ProfileListItem(Profile profile) {
this.profile = profile;
getStyleClass().setAll("profile-list-item", "navigation-drawer-item");
setUserData(profile);
title.set(Profiles.getProfileDisplayName(profile));
subtitle.set(profile.getPath().toString());
this.selectedProperty().bind(Bindings.equal(profile, Profiles.selectedProfileProperty()));
}
@Override
protected Skin<?> createDefaultSkin() {
return new ProfileListItemSkin(this);
}
public void remove() {
if (!Profiles.canRemoveProfile(profile)) {
Controllers.confirmBackupAndOverwrite(i18n("settings.game_directories.read_only"), () -> {
Profiles.forceOverwriteProfileFiles(profile);
Profiles.removeProfile(profile);
});
return;
}
Profiles.removeProfile(profile);
}
public Profile getProfile() {
return profile;
}
public String getTitle() {
return title.get();
}
public void setTitle(String title) {
this.title.set(title);
}
public StringProperty titleProperty() {
return title;
}
public String getSubtitle() {
return subtitle.get();
}
public void setSubtitle(String subtitle) {
this.subtitle.set(subtitle);
}
public StringProperty subtitleProperty() {
return subtitle;
}
}
@@ -38,8 +38,7 @@ import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextFlow;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.terracotta.TerracottaManager;
@@ -213,8 +212,8 @@ public class TerracottaControllerPage extends StackPane {
i18n("terracotta.status.waiting.host.launch.title"),
MessageDialogPane.MessageType.QUESTION
).addAction(i18n("version.launch"), () -> {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, Profiles.getSelectedInstance(profile), launcherHelper -> {
var repository = GameDirectoryManager.getSelectedRepository();
Versions.launch(repository, repository.getSelectedInstance(), launcherHelper -> {
launcherHelper.setKeep();
launcherHelper.setDisableOfflineSkin();
});
@@ -78,13 +78,13 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
AdvancedListBox toolbar = new AdvancedListBox()
.add(accountListItem)
.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> {
Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, Profiles.getSelectedInstance(profile), launcherHelper -> {
var repository = GameDirectoryManager.getSelectedRepository();
Versions.launch(repository, repository.getSelectedInstance(), launcherHelper -> {
launcherHelper.setKeep();
launcherHelper.setDisableOfflineSkin();
});
}, item -> {
instanceChangeListenerHolder = FXUtils.onWeakChangeAndOperate(Profiles.selectedInstanceProperty(),
instanceChangeListenerHolder = FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(),
instanceName -> item.setSubtitle(StringUtils.isNotBlank(instanceName) ? instanceName : i18n("version.empty"))
);
@@ -92,14 +92,14 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa
FXUtils.onScroll(item, mainPage.getVersions(), list -> {
String currentId = mainPage.getCurrentGame();
return Lang.indexWhere(list, instance -> instance.getId().equals(currentId));
}, it -> Profiles.setSelectedInstance(mainPage.getProfile(), it.getId()));
}, it -> mainPage.getRepository().setSelectedInstance(it.getId()));
FXUtils.onSecondaryButtonClicked(item, () -> GameListPopupMenu.show(item,
JFXPopup.PopupVPosition.BOTTOM,
JFXPopup.PopupHPosition.LEFT,
item.getWidth(),
0,
mainPage.getProfile(), mainPage.getVersions()));
mainPage.getRepository(), mainPage.getVersions()));
})
.addNavigationDrawerItem(i18n("terracotta.feedback.title"), SVG.FEEDBACK, () -> FXUtils.openLink(TerracottaMetadata.FEEDBACK_LINK));
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
@@ -38,13 +38,12 @@ import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import org.jackhuang.hmcl.download.DownloadProvider;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -57,6 +56,7 @@ import org.jackhuang.hmcl.util.i18n.I18n;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.net.URI;
import java.util.*;
@@ -67,12 +67,12 @@ import static org.jackhuang.hmcl.ui.FXUtils.stringConverter;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.javafx.ExtendedProperties.selectedItemPropertyFor;
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.VersionLoadable {
public class DownloadListPage extends Control implements DecoratorPage, VersionPage.GameInstanceLoadable {
protected final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>();
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final BooleanProperty failed = new SimpleBooleanProperty(false);
private final boolean versionSelection;
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private final ObjectProperty<HMCLGameRepository.InstanceReference> instanceReference = new SimpleObjectProperty<>();
private final IntegerProperty pageOffset = new SimpleIntegerProperty(0);
private final IntegerProperty pageCount = new SimpleIntegerProperty(-1);
private final ListProperty<RemoteAddon> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
@@ -111,8 +111,8 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
@Override
public void loadVersion(Profile profile, String version) {
this.version.set(new Profile.ProfileVersion(profile, version));
public void loadInstance(HMCLGameRepository repository, @Nullable String instanceId) {
this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, instanceId));
setLoading(false);
setFailed(false);
@@ -123,10 +123,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
if (versionSelection) {
versions.setAll(profile.getRepository().getDisplayVersions()
versions.setAll(repository.getDisplayVersions()
.map(Version::getId)
.collect(Collectors.toList()));
selectedVersion.set(Profiles.getSelectedInstance(profile));
selectedVersion.set(repository.getSelectedInstance());
}
}
@@ -165,12 +165,12 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
int currentSearchID = searchID = searchID + 1;
Task.supplyAsync(() -> {
Profile.ProfileVersion version = this.version.get();
if (StringUtils.isBlank(version.version())) {
HMCLGameRepository.InstanceReference instanceReference = this.instanceReference.get();
if (StringUtils.isBlank(instanceReference.instanceId())) {
return userGameVersion;
} else {
return StringUtils.isNotBlank(version.version())
? version.profile().getRepository().getGameVersion(version.version()).orElse("")
return StringUtils.isNotBlank(instanceReference.instanceId())
? instanceReference.repository().getGameVersion(instanceReference.instanceId()).orElse("")
: "";
}
}).thenApplyAsync(
@@ -218,11 +218,11 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
}
protected Profile.ProfileVersion getProfileVersion() {
protected HMCLGameRepository.InstanceReference getInstanceReference() {
if (versionSelection) {
return new Profile.ProfileVersion(version.get().profile(), selectedVersion.get());
return new HMCLGameRepository.InstanceReference(instanceReference.get().repository(), selectedVersion.get());
} else {
return version.get();
return instanceReference.get();
}
}
@@ -320,16 +320,16 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
Label lblGameVersion = new Label(i18n("world.game_version"));
searchPane.addRow(rowIndex++, new Label(i18n("mods.name")), nameField, lblGameVersion, gameVersionField);
ObjectBinding<Boolean> hasVersion = BindingMapping.of(getSkinnable().version)
.map(version -> version.version() == null);
ObjectBinding<Boolean> hasVersion = BindingMapping.of(getSkinnable().instanceReference)
.map(instanceReference -> instanceReference.instanceId() == null);
lblGameVersion.managedProperty().bind(hasVersion);
lblGameVersion.visibleProperty().bind(hasVersion);
gameVersionField.managedProperty().bind(hasVersion);
gameVersionField.visibleProperty().bind(hasVersion);
FXUtils.installFastTooltip(gameVersionField, i18n("search.enter"));
FXUtils.onChangeAndOperate(getSkinnable().version, version -> {
if (StringUtils.isNotBlank(version.version())) {
FXUtils.onChangeAndOperate(getSkinnable().instanceReference, instanceReference -> {
if (StringUtils.isNotBlank(instanceReference.instanceId())) {
GridPane.setColumnSpan(nameField, 3);
} else {
GridPane.setColumnSpan(nameField, 1);
@@ -561,7 +561,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
FXUtils.onClicked(wrapper, () -> {
RemoteAddon item = getItem();
if (item != null)
Controllers.navigate(new DownloadPage(getSkinnable(), item, getSkinnable().getProfileVersion(), getSkinnable().callback));
Controllers.navigate(new DownloadPage(getSkinnable(), item, getSkinnable().getInstanceReference(), getSkinnable().callback));
});
setPrefWidth(0);
@@ -37,7 +37,6 @@ import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.addon.RemoteAddon;
import org.jackhuang.hmcl.addon.RemoteAddonRepository;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -70,21 +69,21 @@ public class DownloadPage extends Control implements DecoratorPage {
private final ModTranslations translations;
private final RemoteAddon addon;
private final ModTranslations.Mod mod;
private final Profile.ProfileVersion version;
private final HMCLGameRepository.InstanceReference instanceReference;
private final DownloadCallback callback;
private final DownloadListPage page;
private final RemoteAddonRepository.Type type;
private SimpleMultimap<String, RemoteAddon.Version, List<RemoteAddon.Version>> versions;
public DownloadPage(DownloadListPage page, RemoteAddon addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) {
public DownloadPage(DownloadListPage page, RemoteAddon addon, HMCLGameRepository.InstanceReference instanceReference, @Nullable DownloadCallback callback) {
this.page = page;
this.repository = page.repository;
this.addon = addon;
this.type = Objects.requireNonNullElse(addon.repoType(), repository.getType());
this.translations = ModTranslations.getTranslationsByRepositoryType(this.type);
this.mod = translations.getModByCurseForgeId(addon.slug());
this.version = version;
this.instanceReference = instanceReference;
this.callback = callback;
loadAddonVersions();
@@ -131,8 +130,8 @@ public class DownloadPage extends Control implements DecoratorPage {
return addon;
}
public Profile.ProfileVersion getVersion() {
return version;
public HMCLGameRepository.InstanceReference getInstanceReference() {
return instanceReference;
}
public boolean isLoading() {
@@ -163,7 +162,7 @@ public class DownloadPage extends Control implements DecoratorPage {
if (this.callback == null) {
saveAs(file);
} else {
this.callback.download(page.getDownloadProvider(), version.profile(), version.version(), addon, file);
this.callback.download(page.getDownloadProvider(), instanceReference.repository(), instanceReference.instanceId(), addon, file);
}
}
@@ -272,9 +271,9 @@ public class DownloadPage extends Control implements DecoratorPage {
FXUtils.onChangeAndOperate(control.loaded, loaded -> {
if (control.versions == null) return;
if (control.version.profile() != null && control.version.version() != null) {
HMCLGameRepository repository = control.version.profile().getRepository();
Version game = repository.getResolvedPreservingPatchesVersion(control.version.version());
if (control.instanceReference.repository() != null && control.instanceReference.instanceId() != null) {
HMCLGameRepository repository = control.instanceReference.repository();
Version game = repository.getResolvedPreservingPatchesVersion(control.instanceReference.instanceId());
String gameVersion = repository.getGameVersion(game).orElse(null);
if (gameVersion != null && control.versions.containsKey(gameVersion)) {
@@ -374,7 +373,7 @@ public class DownloadPage extends Control implements DecoratorPage {
Pair.pair(RemoteAddon.DependencyType.BROKEN, "addon.dependency.broken")
));
DependencyAddonItem(DownloadListPage page, RemoteAddon addon, Profile.ProfileVersion version) {
DependencyAddonItem(DownloadListPage page, RemoteAddon addon, HMCLGameRepository.InstanceReference instanceReference) {
HBox pane = new HBox(8);
pane.setPadding(new Insets(0, 8, 0, 8));
pane.setAlignment(Pos.CENTER_LEFT);
@@ -394,7 +393,7 @@ public class DownloadPage extends Control implements DecoratorPage {
};
setOnAction((e) -> {
fireEvent(new DialogCloseEvent());
Controllers.navigate(new DownloadPage(page, addon, version, callback));
Controllers.navigate(new DownloadPage(page, addon, instanceReference, callback));
});
setNode(IDX_LEADING, pane);
@@ -587,7 +586,7 @@ public class DownloadPage extends Control implements DecoratorPage {
if (dep == RemoteAddon.BROKEN) {
return;
}
DependencyAddonItem dependencyAddonItem = new DependencyAddonItem(selfPage.page, dep, selfPage.version);
DependencyAddonItem dependencyAddonItem = new DependencyAddonItem(selfPage.page, dep, selfPage.instanceReference);
dependencies.get(dependency.getType()).add(dependencyAddonItem);
})
.setSignificance(Task.TaskSignificance.MINOR));
@@ -611,6 +610,6 @@ public class DownloadPage extends Control implements DecoratorPage {
@FunctionalInterface
public interface DownloadCallback {
void download(DownloadProvider downloadProvider, Profile profile, @Nullable String version, RemoteAddon addon, RemoteAddon.Version file);
void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable String version, RemoteAddon addon, RemoteAddon.Version file);
}
}
@@ -19,8 +19,8 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.geometry.Pos;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.WeakListenerHolder;
@@ -34,7 +34,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameAdvancedListItem extends AdvancedListItem {
private final ImageContainer imageContainer;
private final WeakListenerHolder holder = new WeakListenerHolder();
private Profile profile;
private HMCLGameRepository repository;
@SuppressWarnings("unused")
private Consumer<Event> onVersionIconChangedListener;
@@ -44,23 +44,22 @@ public class GameAdvancedListItem extends AdvancedListItem {
AdvancedListItem.setAlignment(imageContainer, Pos.CENTER);
setLeftGraphic(imageContainer);
holder.add(FXUtils.onWeakChangeAndOperate(Profiles.selectedInstanceProperty(), this::loadVersion));
holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), this::loadVersion));
}
private void loadVersion(String version) {
if (Profiles.getSelectedProfile() != profile) {
profile = Profiles.getSelectedProfile();
if (profile != null) {
onVersionIconChangedListener = profile.getRepository().onVersionIconChanged.registerWeak(event -> {
this.loadVersion(Profiles.getSelectedInstance());
if (GameDirectoryManager.getSelectedRepository() != repository) {
repository = GameDirectoryManager.getSelectedRepository();
if (repository != null) {
onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> {
this.loadVersion(repository.getSelectedInstance());
});
}
}
if (version != null && Profiles.getSelectedProfile() != null &&
Profiles.getSelectedProfile().getRepository().hasVersion(version)) {
if (version != null && repository != null && repository.hasVersion(version)) {
setTitle(i18n("version.manage.manage"));
setSubtitle(version);
imageContainer.setImage(Profiles.getSelectedProfile().getRepository().getVersionIconImage(version));
imageContainer.setImage(repository.getVersionIconImage(version));
} else {
setTitle(i18n("version.empty"));
setSubtitle(i18n("version.empty.add"));
@@ -20,8 +20,9 @@ package org.jackhuang.hmcl.ui.versions;
import javafx.beans.property.*;
import javafx.scene.image.Image;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.modpack.ModpackConfiguration;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.util.i18n.I18n;
import org.jetbrains.annotations.Nullable;
@@ -41,7 +42,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public class GameItem {
private static final ThreadPoolExecutor POOL_VERSION_RESOLVE = threadPool("VersionResolve", true, 1, 10, TimeUnit.SECONDS);
protected final Profile profile;
protected final HMCLGameRepository repository;
protected final String id;
private boolean initialized = false;
@@ -50,13 +51,17 @@ public class GameItem {
private StringProperty subtitle;
private ObjectProperty<Image> image;
public GameItem(Profile profile, String id) {
this.profile = profile;
public GameItem(HMCLGameRepository repository, String id) {
this.repository = repository;
this.id = id;
}
public Profile getProfile() {
return profile;
public GameDirectory getGameDirectory() {
return repository.getGameDirectory();
}
public HMCLGameRepository getRepository() {
return repository;
}
public String getId() {
@@ -78,10 +83,10 @@ public class GameItem {
CompletableFuture.supplyAsync(() -> {
// GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms)
Optional<String> gameVersion = profile.getRepository().getGameVersion(id);
Optional<String> gameVersion = repository.getGameVersion(id);
String modPackVersion = null;
try {
ModpackConfiguration<?> config = profile.getRepository().readModpackConfiguration(id);
ModpackConfiguration<?> config = repository.readModpackConfiguration(id);
modPackVersion = config != null ? config.getVersion() : null;
} catch (IOException e) {
LOG.warning("Failed to read modpack configuration from " + id, e);
@@ -94,7 +99,7 @@ public class GameItem {
}
StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown")));
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(id), result.gameVersion);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(id), result.gameVersion);
for (LibraryAnalyzer.LibraryMark mark : analyzer) {
String libraryId = mark.getLibraryId();
String libraryVersion = mark.getLibraryVersion();
@@ -113,7 +118,7 @@ public class GameItem {
}, Schedulers.javafx());
title.set(id);
image.set(profile.getRepository().getVersionIconImage(id));
image.set(repository.getVersionIconImage(id));
}
public ReadOnlyStringProperty titleProperty() {
@@ -29,7 +29,6 @@ import javafx.scene.input.MouseButton;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.*;
@@ -70,7 +69,7 @@ public final class GameListCell extends ListCell<GameListItem> {
fireEvent(new ActionEvent());
GameListItem item = GameListCell.this.getItem();
if (item != null) {
Profiles.setSelectedInstance(item.getProfile(), item.getId());
item.getRepository().setSelectedInstance(item.getId());
}
}
}
@@ -21,8 +21,8 @@ import javafx.beans.binding.Bindings;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyBooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import java.util.Objects;
@@ -30,13 +30,13 @@ public class GameListItem extends GameItem {
private final boolean isModpack;
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected");
public GameListItem(Profile profile, String id) {
super(profile, id);
this.isModpack = profile.getRepository().isModpack(id);
public GameListItem(HMCLGameRepository repository, String id) {
super(repository, id);
this.isModpack = repository.isModpack(id);
selected.bind(Bindings.createBooleanBinding(
() -> profile == Profiles.getSelectedProfile() && Objects.equals(Profiles.getSelectedInstance(profile), id),
Profiles.selectedProfileProperty(),
Profiles.selectedInstanceProperty()));
() -> repository.getGameDirectory() == GameDirectoryManager.getSelectedGameDirectory() && Objects.equals(repository.getSelectedInstance(), id),
GameDirectoryManager.selectedGameDirectoryProperty(),
GameDirectoryManager.selectedInstanceProperty()));
}
public ReadOnlyBooleanProperty selectedProperty() {
@@ -44,39 +44,39 @@ public class GameListItem extends GameItem {
}
public void rename() {
Versions.renameVersion(profile, id);
Versions.renameVersion(repository, id);
}
public void duplicate() {
Versions.duplicateVersion(profile, id);
Versions.duplicateVersion(repository, id);
}
public void remove() {
Versions.deleteVersion(profile, id);
Versions.deleteVersion(repository, id);
}
public void export() {
Versions.exportVersion(profile, id);
Versions.exportVersion(repository, id);
}
public void browse() {
Versions.openFolder(profile, id);
Versions.openFolder(repository, id);
}
public void testGame() {
Versions.testGame(profile, id);
Versions.testGame(repository, id);
}
public void launch() {
Versions.launch(profile, id);
Versions.launch(repository, id);
}
public void modifyGameSettings() {
Versions.modifyGameSettings(profile, id);
Versions.modifyGameSettings(repository, id);
}
public void generateLaunchScript() {
Versions.generateLaunchScript(profile, id);
Versions.generateLaunchScript(repository, id);
}
public boolean canUpdate() {
@@ -84,6 +84,6 @@ public class GameListItem extends GameItem {
}
public void update() {
Versions.updateVersion(profile, id);
Versions.updateVersion(repository, id);
}
}
@@ -39,9 +39,9 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.ModpackHelper;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import org.jackhuang.hmcl.ui.*;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionPane;
@@ -52,8 +52,8 @@ import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider;
import org.jackhuang.hmcl.ui.profile.ProfileListItem;
import org.jackhuang.hmcl.ui.profile.ProfilePage;
import org.jackhuang.hmcl.ui.directory.GameDirectoryListItem;
import org.jackhuang.hmcl.ui.directory.GameDirectoryPage;
import org.jackhuang.hmcl.util.FXThread;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
@@ -71,12 +71,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage")));
/// Navigation drawer items for configured game directories.
@SuppressWarnings("FieldCanBeLocal")
private final ObservableList<ProfileListItem> profileListItems;
private final ObservableList<GameDirectoryListItem> gameDirectoryListItems;
public GameListPage() {
profileListItems = MappedObservableList.create(Profiles.getProfiles(), profile -> {
ProfileListItem item = new ProfileListItem(profile);
gameDirectoryListItems = MappedObservableList.create(GameDirectoryManager.getGameDirectories(), gameDirectory -> {
GameDirectoryListItem item = new GameDirectoryListItem(gameDirectory);
FXUtils.setLimitWidth(item, 200);
return item;
});
@@ -85,19 +86,19 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
ScrollPane pane = new ScrollPane();
VBox.setVgrow(pane, Priority.ALWAYS);
{
AdvancedListItem addProfileItem = new AdvancedListItem();
addProfileItem.getStyleClass().add("navigation-drawer-item");
addProfileItem.setTitle(i18n("profile.new"));
addProfileItem.setLeftIcon(SVG.ADD_CIRCLE);
addProfileItem.setOnAction(e -> Controllers.navigate(new ProfilePage(null)));
AdvancedListItem addGameDirectoryItem = new AdvancedListItem();
addGameDirectoryItem.getStyleClass().add("navigation-drawer-item");
addGameDirectoryItem.setTitle(i18n("game_directory.new"));
addGameDirectoryItem.setLeftIcon(SVG.ADD_CIRCLE);
addGameDirectoryItem.setOnAction(e -> Controllers.navigate(new GameDirectoryPage(null)));
pane.setFitToWidth(true);
VBox wrapper = new VBox();
wrapper.getStyleClass().add("advanced-list-box-content");
VBox box = new VBox();
box.setFillWidth(true);
Bindings.bindContent(box.getChildren(), profileListItems);
wrapper.getChildren().setAll(box, addProfileItem);
Bindings.bindContent(box.getChildren(), gameDirectoryListItems);
wrapper.getChildren().setAll(box, addGameDirectoryItem);
pane.setContent(wrapper);
}
@@ -115,15 +116,15 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
Path file = files.get(0);
if (ModpackHelper.isFileModpackByExtension(file)) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(Profiles.getSelectedProfile(), file), i18n("install.modpack"));
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(GameDirectoryManager.getSelectedRepository(), file), i18n("install.modpack"));
} else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) {
Versions.installFromJson(Profiles.getSelectedProfile(), file);
Versions.installFromJson(GameDirectoryManager.getSelectedRepository(), file);
}
});
}
public void modifyGlobalGameSettings() {
Versions.modifyGlobalSettings(Profiles.getSelectedProfile());
Versions.modifyGlobalSettings(GameDirectoryManager.getSelectedRepository());
}
@Override
@@ -140,18 +141,18 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
public GameList() {
setItems(filteredList);
Profiles.registerVersionsListener(this::loadVersions);
GameDirectoryManager.registerVersionsListener(this::loadVersions);
setOnFailedAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
}
@FXThread
private void loadVersions(Profile profile) {
private void loadVersions(HMCLGameRepository repository) {
listenerHolder.clear();
setLoading(true);
setFailedReason(null);
List<GameListItem> versionItems = profile.getRepository().getDisplayVersions().map(instance -> new GameListItem(profile, instance.getId())).toList();
List<GameListItem> versionItems = repository.getDisplayVersions().map(instance -> new GameListItem(repository, instance.getId())).toList();
sourceList.setAll(versionItems);
@@ -181,7 +182,7 @@ public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage
}
public void refreshList() {
Profiles.getSelectedProfile().getRepository().refreshVersionsAsync().start();
GameDirectoryManager.getSelectedRepository().refreshVersionsAsync().start();
}
@Override
@@ -33,9 +33,8 @@ import javafx.scene.control.ListView;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.Profiles;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.ImageContainer;
import org.jackhuang.hmcl.ui.construct.RipplerContainer;
@@ -51,11 +50,11 @@ public final class GameListPopupMenu extends StackPane {
public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign,
double initOffsetX, double initOffsetY,
Profile profile, List<Version> versions) {
HMCLGameRepository repository, List<Version> versions) {
GameListPopupMenu menu = new GameListPopupMenu();
menu.getItems().setAll(versions.stream()
.filter(it -> profile.getRepository().hasVersion(it.getId()))
.map(it -> new GameItem(profile, it.getId()))
.filter(it -> repository.hasVersion(it.getId()))
.map(it -> new GameItem(repository, it.getId()))
.toList());
JFXPopup popup = new JFXPopup(menu);
popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY);
@@ -128,7 +127,7 @@ public final class GameListPopupMenu extends StackPane {
FXUtils.onClicked(rootPane, () -> {
GameItem item = getItem();
if (item != null) {
Profiles.setSelectedInstance(item.getProfile(), item.getId());
item.getRepository().setSelectedInstance(item.getId());
if (getScene().getWindow() instanceof JFXPopup popup)
popup.hide();
}
@@ -22,8 +22,8 @@ import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.Version;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
@@ -42,8 +42,8 @@ import java.util.concurrent.CompletableFuture;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class InstallerListPage extends ListPageBase<InstallerItem> implements VersionPage.VersionLoadable {
private Profile profile;
public class InstallerListPage extends ListPageBase<InstallerItem> implements VersionPage.GameInstanceLoadable {
private HMCLGameRepository repository;
private String versionId;
private Version version;
private String gameVersion;
@@ -61,16 +61,16 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
}
@Override
public void loadVersion(Profile profile, String versionId) {
this.profile = profile;
this.versionId = versionId;
this.version = profile.getRepository().getVersion(versionId);
public void loadInstance(HMCLGameRepository repository, String instanceId) {
this.repository = repository;
this.versionId = instanceId;
this.version = repository.getVersion(instanceId);
this.gameVersion = null;
CompletableFuture.supplyAsync(() -> {
gameVersion = profile.getRepository().getGameVersion(version).orElse(null);
gameVersion = repository.getGameVersion(version).orElse(null);
return LibraryAnalyzer.analyze(profile.getRepository().getResolvedPreservingPatchesVersion(versionId), gameVersion);
return LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(instanceId), gameVersion);
}).thenAcceptAsync(analyzer -> {
itemsProperty().clear();
@@ -98,13 +98,13 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
}
item.setOnInstall(() -> {
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(profile, gameVersion, version, libraryId, libraryVersion));
Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(repository, gameVersion, version, libraryId, libraryVersion));
});
item.setOnRemove(() -> profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::saveAsync)
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
item.setOnRemove(() -> repository.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(repository::saveAsync)
.withComposeAsync(repository.refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.versionId))
.start());
itemsProperty().add(item);
@@ -123,10 +123,10 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
InstallerItem installerItem = new InstallerItem(libraryId, InstallerItem.Style.LIST_ITEM);
installerItem.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false));
installerItem.setOnRemove(() -> profile.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(profile.getRepository()::saveAsync)
.withComposeAsync(profile.getRepository().refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadVersion(this.profile, this.versionId))
installerItem.setOnRemove(() -> repository.getDependency().removeLibraryAsync(version, libraryId)
.thenComposeAsync(repository::saveAsync)
.withComposeAsync(repository.refreshVersionsAsync())
.withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.versionId))
.start());
itemsProperty().add(installerItem);
@@ -142,16 +142,16 @@ public class InstallerListPage extends ListPageBase<InstallerItem> implements Ve
}
private void doInstallOffline(Path file) {
Task<?> task = profile.getDependency().installLibraryAsync(version, file)
.thenComposeAsync(profile.getRepository()::saveAsync)
.thenComposeAsync(profile.getRepository().refreshVersionsAsync());
Task<?> task = repository.getDependency().installLibraryAsync(version, file)
.thenComposeAsync(repository::saveAsync)
.thenComposeAsync(repository.refreshVersionsAsync());
task.setName(i18n("install.installer.install_offline"));
TaskExecutor executor = task.executor(new TaskListener() {
@Override
public void onStop(boolean success, TaskExecutor executor) {
runInFX(() -> {
if (success) {
loadVersion(profile, versionId);
loadInstance(repository, versionId);
Controllers.dialog(i18n("install.success"));
} else {
if (executor.getException() == null)
@@ -29,7 +29,7 @@ import org.jackhuang.hmcl.addon.mod.LocalModFile;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.addon.mod.ModManager;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.GameDirectory;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -51,13 +51,13 @@ import java.util.concurrent.locks.ReentrantLock;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.VersionLoadable, PageAware {
public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObject> implements VersionPage.GameInstanceLoadable, PageAware {
private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false);
private final ReentrantLock lock = new ReentrantLock();
private ModManager modManager;
private Profile profile;
private HMCLGameRepository repository;
private String instanceId;
private String gameVersion;
@@ -86,16 +86,15 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
@Override
public void loadVersion(Profile profile, String id) {
this.profile = profile;
this.instanceId = id;
public void loadInstance(HMCLGameRepository repository, String instanceId) {
this.repository = repository;
this.instanceId = instanceId;
HMCLGameRepository repository = profile.getRepository();
Version resolved = repository.getResolvedPreservingPatchesVersion(id);
Version resolved = repository.getResolvedPreservingPatchesVersion(instanceId);
this.gameVersion = repository.getGameVersion(resolved).orElse(null);
LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion);
modded.set(analyzer.hasModLoader());
loadMods(profile.getRepository().getModManager(id));
loadMods(repository.getModManager(instanceId));
}
private void loadMods(ModManager modManager) {
@@ -232,14 +231,14 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
}
public void openModFolder() {
FXUtils.openFolder(profile.getRepository().getRunDirectory(instanceId).resolve("mods"));
FXUtils.openFolder(repository.getRunDirectory(instanceId).resolve("mods"));
}
public void checkUpdates(Collection<LocalModFile> mods) {
Objects.requireNonNull(mods);
Runnable action = () -> Controllers.taskDialog(Task
.composeAsync(() -> {
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
Optional<String> gameVersion = repository.getGameVersion(instanceId);
return gameVersion.map(g -> new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(), g, mods)).orElse(null);
})
.whenComplete(Schedulers.javafx(), (result, exception) -> {
@@ -255,7 +254,7 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
.withStagesHints("update.checking"),
i18n("addon.check_update"), TaskCancellationAction.NORMAL);
if (profile.getRepository().isModpack(instanceId)) {
if (repository.isModpack(instanceId)) {
Controllers.confirm(
i18n("mods.update_modpack_mod.warning"), null,
MessageDialogPane.MessageType.WARNING,
@@ -291,8 +290,12 @@ public final class ModListPage extends ListPageBase<ModListPageSkin.ModInfoObjec
this.modded.set(modded);
}
public Profile getProfile() {
return this.profile;
public GameDirectory getGameDirectory() {
return this.repository.getGameDirectory();
}
public HMCLGameRepository getRepository() {
return this.repository;
}
public String getInstanceId() {
@@ -42,8 +42,8 @@ import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
import org.jackhuang.hmcl.addon.mod.LocalModFile;
import org.jackhuang.hmcl.addon.mod.ModLoaderType;
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
@@ -502,7 +502,7 @@ final class ModListPageSkin extends SkinBase<ModListPage> {
Controllers.navigate(new DownloadPage(
repository instanceof CurseForgeRemoteAddonRepository ? HMCLLocalizedDownloadListPage.ofCurseForgeMod(null, false) : HMCLLocalizedDownloadListPage.ofModrinthMod(null, false),
remoteAddon,
new Profile.ProfileVersion(ModListPageSkin.this.getSkinnable().getProfile(), ModListPageSkin.this.getSkinnable().getInstanceId()),
new HMCLGameRepository.InstanceReference(ModListPageSkin.this.getSkinnable().getRepository(), ModListPageSkin.this.getSkinnable().getInstanceId()),
org.jackhuang.hmcl.ui.download.DownloadPage.FOR_MOD
));
});
@@ -42,9 +42,9 @@ import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository;
import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository;
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackFile;
import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.SettingsManager;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -74,7 +74,7 @@ import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class ResourcePackListPage extends ListPageBase<ResourcePackListPage.ResourcePackInfoObject> implements VersionPage.VersionLoadable {
public final class ResourcePackListPage extends ListPageBase<ResourcePackListPage.ResourcePackInfoObject> implements VersionPage.GameInstanceLoadable {
private static final String TIP_KEY = "resourcePackWarning";
private static @Nullable String getWarning(ResourcePackFile.Compatibility compatibility) {
@@ -88,7 +88,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
};
}
private Profile profile;
private HMCLGameRepository repository;
private String instanceId;
private Path resourcePackDirectory;
@@ -106,10 +106,10 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
}
@Override
public void loadVersion(Profile profile, String version) {
this.profile = profile;
this.instanceId = version;
this.resourcePackManager = new ResourcePackManager(profile.getRepository(), version);
public void loadInstance(HMCLGameRepository repository, String instanceId) {
this.repository = repository;
this.instanceId = instanceId;
this.resourcePackManager = new ResourcePackManager(repository, instanceId);
this.resourcePackDirectory = this.resourcePackManager.getDirectory();
refresh();
@@ -232,7 +232,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
public void checkUpdates(Collection<ResourcePackFile> resourcePacks) {
Runnable action = () -> Controllers.taskDialog(Task
.composeAsync(() -> {
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
Optional<String> gameVersion = repository.getGameVersion(instanceId);
return gameVersion.map(g -> new AddonCheckUpdatesTask<>(DownloadProviders.getDownloadProvider(), g, resourcePacks)).orElse(null);
})
.whenComplete(Schedulers.javafx(), (result, exception) -> {
@@ -247,7 +247,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
.withStagesHints("update.checking"),
i18n("addon.check_update"), TaskCancellationAction.NORMAL);
if (profile.getRepository().isModpack(instanceId)) {
if (repository.isModpack(instanceId)) {
Controllers.confirm(
i18n("resourcepack.update_in_modpack.warning"), null,
MessageDialogPane.MessageType.WARNING,
@@ -619,7 +619,7 @@ public final class ResourcePackListPage extends ListPageBase<ResourcePackListPag
? HMCLLocalizedDownloadListPage.ofCurseForgeResourcePack(null, false)
: HMCLLocalizedDownloadListPage.ofModrinthResourcePack(null, false),
remoteAddon,
new Profile.ProfileVersion(page.profile, page.instanceId),
new HMCLGameRepository.InstanceReference(page.repository, page.instanceId),
org.jackhuang.hmcl.ui.download.DownloadPage.FOR_RESOURCE_PACK
));
});
@@ -34,8 +34,8 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.schematic.LitematicFile;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
@@ -62,7 +62,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
/**
* @author Glavo
*/
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> implements VersionPage.VersionLoadable {
public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> implements VersionPage.GameInstanceLoadable {
private static String translateAuthorName(String author) {
if (I18n.isUseChinese() && "hsds".equals(author)) {
@@ -87,8 +87,8 @@ public final class SchematicsPage extends ListPageBase<SchematicsPage.Item> impl
}
@Override
public void loadVersion(Profile profile, String version) {
this.schematicsDirectory = profile.getRepository().getSchematicsDirectory(version);
public void loadInstance(HMCLGameRepository repository, String instanceId) {
this.schematicsDirectory = repository.getSchematicsDirectory(instanceId);
refresh();
}
@@ -22,8 +22,8 @@ import javafx.scene.image.ImageView;
import javafx.scene.layout.FlowPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
@@ -39,16 +39,16 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class VersionIconDialog extends DialogPane {
private final Profile profile;
private final HMCLGameRepository repository;
private final String versionId;
private final Runnable onFinish;
private final GameSettings.Instance setting;
public VersionIconDialog(Profile profile, String versionId, Runnable onFinish) {
this.profile = profile;
public VersionIconDialog(HMCLGameRepository repository, String versionId, Runnable onFinish) {
this.repository = repository;
this.versionId = versionId;
this.onFinish = onFinish;
this.setting = profile.getRepository().getInstanceGameSettingsOrCreate(versionId);
this.setting = repository.getInstanceGameSettingsOrCreate(versionId);
setTitle(i18n("settings.icon"));
FlowPane pane = new FlowPane();
@@ -79,7 +79,7 @@ public class VersionIconDialog extends DialogPane {
Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (selectedFile != null) {
try {
profile.getRepository().setVersionIconFile(versionId, selectedFile);
repository.setVersionIconFile(versionId, selectedFile);
if (setting != null) {
setting.iconProperty().setValue(VersionIconType.DEFAULT);
@@ -119,7 +119,7 @@ public class VersionIconDialog extends DialogPane {
@Override
protected void onAccept() {
profile.getRepository().onVersionIconChanged.fireEvent(new Event(this));
repository.onVersionIconChanged.fireEvent(new Event(this));
onFinish.run();
super.onAccept();
}
@@ -29,9 +29,8 @@ import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.event.EventBus;
import org.jackhuang.hmcl.event.EventPriority;
import org.jackhuang.hmcl.event.RefreshedVersionsEvent;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.setting.GameSettings;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers;
@@ -45,6 +44,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.ui.game.GameSettingsPage;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import org.jetbrains.annotations.Nullable;
import java.nio.file.Path;
import java.util.Optional;
@@ -64,7 +64,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private final TabHeader.Tab<ResourcePackListPage> resourcePackTab = new TabHeader.Tab<>("resourcePackTab");
private final TransitionPane transitionPane = new TransitionPane();
private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty();
private final ObjectProperty<Profile.ProfileVersion> version = new SimpleObjectProperty<>();
private final ObjectProperty<HMCLGameRepository.InstanceReference> instanceReference = new SimpleObjectProperty<>();
private final WeakListenerHolder listenerHolder = new WeakListenerHolder();
private String preferredVersionName = null;
@@ -91,17 +91,17 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated);
addEventHandler(WorkingDirChangedEvent.EVENT_TYPE, event -> {
if (this.version.get() != null) {
if (this.instanceReference.get() != null) {
if (installerListTab.isInitialized())
installerListTab.getNode().loadVersion(getProfile(), getVersion());
installerListTab.getNode().loadInstance(getRepository(), getVersion());
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(getProfile(), getVersion());
modListTab.getNode().loadInstance(getRepository(), getVersion());
if (resourcePackTab.isInitialized())
resourcePackTab.getNode().loadVersion(getProfile(), getVersion());
resourcePackTab.getNode().loadInstance(getRepository(), getVersion());
if (worldListTab.isInitialized())
worldListTab.getNode().loadVersion(getProfile(), getVersion());
worldListTab.getNode().loadInstance(getRepository(), getVersion());
if (schematicsTab.isInitialized())
schematicsTab.getNode().loadVersion(getProfile(), getVersion());
schematicsTab.getNode().loadInstance(getRepository(), getVersion());
}
});
@@ -110,11 +110,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private void checkSelectedVersion() {
runInFX(() -> {
if (this.version.get() == null) return;
GameRepository repository = this.version.get().profile().getRepository();
if (!repository.hasVersion(this.version.get().version())) {
if (this.instanceReference.get() == null) return;
HMCLGameRepository repository = this.instanceReference.get().repository();
if (!repository.hasVersion(this.instanceReference.get().instanceId())) {
if (preferredVersionName != null) {
loadVersion(preferredVersionName, this.version.get().profile());
loadVersion(preferredVersionName, repository);
} else {
fireEvent(new PageCloseEvent());
}
@@ -125,9 +125,9 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
private <T extends Node> Supplier<T> loadVersionFor(Supplier<T> nodeSupplier) {
return () -> {
T node = nodeSupplier.get();
if (version.get() != null) {
if (node instanceof VersionPage.VersionLoadable) {
((VersionLoadable) node).loadVersion(version.get().profile(), version.get().version());
if (instanceReference.get() != null) {
if (node instanceof VersionPage.GameInstanceLoadable) {
((GameInstanceLoadable) node).loadInstance(instanceReference.get().repository(), instanceReference.get().instanceId());
}
}
return node;
@@ -138,62 +138,62 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
tab.select(versionSettingsTab, false);
}
public void setVersion(String version, Profile profile) {
this.version.set(new Profile.ProfileVersion(profile, version));
public void setVersion(String version, HMCLGameRepository repository) {
this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, version));
}
public void loadVersion(String version, Profile profile) {
public void loadVersion(String version, HMCLGameRepository repository) {
// If we jumped to game list page and deleted this version
// and back to this page, we should return to main page.
if (this.version.get() != null && (!getProfile().getRepository().isLoaded() ||
!getProfile().getRepository().hasVersion(version))) {
if (this.instanceReference.get() != null && (!getRepository().isLoaded() ||
!getRepository().hasVersion(version))) {
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
return;
}
setVersion(version, profile);
setVersion(version, repository);
preferredVersionName = version;
if (versionSettingsTab.isInitialized())
versionSettingsTab.getNode().loadVersion(profile, version);
versionSettingsTab.getNode().loadInstance(repository, version);
if (installerListTab.isInitialized())
installerListTab.getNode().loadVersion(profile, version);
installerListTab.getNode().loadInstance(repository, version);
if (modListTab.isInitialized())
modListTab.getNode().loadVersion(profile, version);
modListTab.getNode().loadInstance(repository, version);
if (resourcePackTab.isInitialized())
resourcePackTab.getNode().loadVersion(profile, version);
resourcePackTab.getNode().loadInstance(repository, version);
if (worldListTab.isInitialized())
worldListTab.getNode().loadVersion(profile, version);
worldListTab.getNode().loadInstance(repository, version);
if (schematicsTab.isInitialized())
schematicsTab.getNode().loadVersion(profile, version);
currentVersionUpgradable.set(profile.getRepository().isModpack(version));
schematicsTab.getNode().loadInstance(repository, version);
currentVersionUpgradable.set(repository.isModpack(version));
}
private void onNavigated(Navigator.NavigationEvent event) {
if (this.version.get() == null)
if (this.instanceReference.get() == null)
throw new IllegalStateException();
// If we jumped to game list page and deleted this version
// and back to this page, we should return to main page.
if (!getProfile().getRepository().isLoaded() ||
!getProfile().getRepository().hasVersion(getVersion())) {
if (!getRepository().isLoaded() ||
!getRepository().hasVersion(getVersion())) {
Platform.runLater(() -> fireEvent(new PageCloseEvent()));
return;
}
loadVersion(getVersion(), getProfile());
loadVersion(getVersion(), getRepository());
}
private void onBrowse(String sub) {
FXUtils.openFolder(getProfile().getRepository().getRunDirectory(getVersion()).resolve(sub));
FXUtils.openFolder(getRepository().getRunDirectory(getVersion()).resolve(sub));
}
private void redownloadAssetIndex() {
Versions.updateGameAssets(getProfile(), getVersion());
Versions.updateGameAssets(getRepository(), getVersion());
}
private void clearLibraries() {
var libraries = getProfile().getRepository().getBaseDirectory().resolve("libraries");
var libraries = getRepository().getBaseDirectory().resolve("libraries");
Task.runAsync(Schedulers.io(), () -> {
FileUtils.deleteDirectoryQuietly(libraries);
}).whenComplete(Schedulers.javafx(), (exception) -> {
@@ -204,11 +204,11 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
private void clearAssets() {
Path assetsDir = getProfile().getRepository().getBaseDirectory().resolve("assets");
Path assetsDir = getRepository().getBaseDirectory().resolve("assets");
Profile.ProfileVersion currentVersion = version.get();
Path resourcesDir = currentVersion != null
? getProfile().getRepository().getRunDirectory(currentVersion.version()).resolve("resources")
HMCLGameRepository.InstanceReference currentInstanceReference = instanceReference.get();
Path resourcesDir = currentInstanceReference != null
? getRepository().getRunDirectory(currentInstanceReference.instanceId()).resolve("resources")
: null;
Task.runAsync(Schedulers.io(), () -> {
@@ -224,44 +224,44 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
private void clearJunkFiles() {
Versions.cleanVersion(getProfile(), getVersion());
Versions.cleanVersion(getRepository(), getVersion());
}
private void testGame() {
Versions.testGame(getProfile(), getVersion());
Versions.testGame(getRepository(), getVersion());
}
private void updateGame() {
Versions.updateVersion(getProfile(), getVersion());
Versions.updateVersion(getRepository(), getVersion());
}
private void generateLaunchScript() {
Versions.generateLaunchScript(getProfile(), getVersion());
Versions.generateLaunchScript(getRepository(), getVersion());
}
private void export() {
Versions.exportVersion(getProfile(), getVersion());
Versions.exportVersion(getRepository(), getVersion());
}
private void rename() {
Versions.renameVersion(getProfile(), getVersion())
Versions.renameVersion(getRepository(), getVersion())
.thenApply(newVersionName -> this.preferredVersionName = newVersionName);
}
private void remove() {
Versions.deleteVersion(getProfile(), getVersion());
Versions.deleteVersion(getRepository(), getVersion());
}
private void duplicate() {
Versions.duplicateVersion(getProfile(), getVersion());
Versions.duplicateVersion(getRepository(), getVersion());
}
public Profile getProfile() {
return Optional.ofNullable(version.get()).map(Profile.ProfileVersion::profile).orElse(null);
public HMCLGameRepository getRepository() {
return Optional.ofNullable(instanceReference.get()).map(HMCLGameRepository.InstanceReference::repository).orElse(null);
}
public String getVersion() {
return Optional.ofNullable(version.get()).map(Profile.ProfileVersion::version).orElse(null);
return Optional.ofNullable(instanceReference.get()).map(HMCLGameRepository.InstanceReference::instanceId).orElse(null);
}
@Override
@@ -345,7 +345,7 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
control.state.bind(Bindings.createObjectBinding(() ->
State.fromTitle(i18n("version.manage.manage.title", getSkinnable().getVersion()), -1),
getSkinnable().version));
getSkinnable().instanceReference));
//control.transitionPane.getStyleClass().add("gray-background");
//FXUtils.setOverflowHidden(control.transitionPane, 8);
@@ -353,7 +353,12 @@ public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage
}
}
public interface VersionLoadable {
void loadVersion(Profile profile, String version);
/// Loads page content for a game instance in a repository.
public interface GameInstanceLoadable {
/// Loads page content for the given repository and game instance.
///
/// @param repository the repository containing the game instance
/// @param instanceId the game instance ID, or `null` when only repository context is available
void loadInstance(HMCLGameRepository repository, @Nullable String instanceId);
}
}
@@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.ui.versions;
import org.jackhuang.hmcl.setting.GameDirectoryManager;
import com.jfoenix.controls.JFXButton;
import javafx.application.Platform;
import javafx.stage.FileChooser;
@@ -70,13 +72,13 @@ public final class Versions {
}
public static void importModpack() {
Profile profile = Profiles.getSelectedProfile();
if (profile.getRepository().isLoaded()) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile), i18n("install.modpack"));
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
if (repository.isLoaded()) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository), i18n("install.modpack"));
}
}
public static void downloadModpackImpl(DownloadProvider downloadProvider, Profile profile, String version, RemoteAddon mod, RemoteAddon.Version file) {
public static void downloadModpackImpl(DownloadProvider downloadProvider, HMCLGameRepository repository, String version, RemoteAddon mod, RemoteAddon.Version file) {
Path modpack;
List<URI> downloadURLs;
try {
@@ -94,9 +96,9 @@ public final class Versions {
if (e == null) {
ModpackInstallWizardProvider installWizardProvider;
if (version != null)
installWizardProvider = new ModpackInstallWizardProvider(profile, modpack, version);
installWizardProvider = new ModpackInstallWizardProvider(repository, modpack, version);
else
installWizardProvider = new ModpackInstallWizardProvider(profile, modpack);
installWizardProvider = new ModpackInstallWizardProvider(repository, modpack);
if (StringUtils.isNotBlank(mod.iconUrl()))
installWizardProvider.setIconUrl(mod.iconUrl());
Controllers.getDecorator().startWizard(installWizardProvider);
@@ -113,16 +115,16 @@ public final class Versions {
);
}
public static void deleteVersion(Profile profile, String version) {
boolean isIndependent = profile.getRepository().getRunDirectory(version).toAbsolutePath().normalize()
.equals(profile.getRepository().getVersionRoot(version).toAbsolutePath().normalize());
public static void deleteVersion(HMCLGameRepository repository, String version) {
boolean isIndependent = repository.getRunDirectory(version).toAbsolutePath().normalize()
.equals(repository.getVersionRoot(version).toAbsolutePath().normalize());
String message = isIndependent ? i18n("version.manage.remove.confirm.independent", version) :
i18n("version.manage.remove.confirm.trash", version, version + "_removed");
JFXButton deleteButton = new JFXButton(i18n("button.delete"));
deleteButton.getStyleClass().add("dialog-error");
deleteButton.setOnAction(e -> {
Task.supplyAsync(Schedulers.io(), () -> profile.getRepository().removeVersionFromDisk(version))
Task.supplyAsync(Schedulers.io(), () -> repository.removeVersionFromDisk(version))
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception != null || !Boolean.TRUE.equals(result)) {
Controllers.dialog(i18n("version.manage.remove.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
@@ -133,18 +135,18 @@ public final class Versions {
Controllers.confirmAction(message, i18n("message.warning"), MessageDialogPane.MessageType.WARNING, deleteButton);
}
public static CompletableFuture<String> renameVersion(Profile profile, String version) {
public static CompletableFuture<String> renameVersion(HMCLGameRepository repository, String version) {
return Controllers.prompt(i18n("version.manage.rename.message"), (newName, handler) -> {
if (newName.equals(version)) {
handler.resolve();
return;
}
if (profile.getRepository().renameVersion(version, newName)) {
if (repository.renameVersion(version, newName)) {
handler.resolve();
profile.getRepository().refreshVersionsAsync()
repository.refreshVersionsAsync()
.thenRunAsync(Schedulers.javafx(), () -> {
if (profile.getRepository().hasVersion(newName)) {
Profiles.setSelectedInstance(profile, newName);
if (repository.hasVersion(newName)) {
repository.setSelectedInstance(newName);
}
}).start();
} else {
@@ -152,21 +154,21 @@ public final class Versions {
}
}, version,
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName) || newVersionName.equals(version)));
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.versionIdConflicts(newVersionName) || newVersionName.equals(version)));
}
public static void exportVersion(Profile profile, String version) {
Controllers.getDecorator().startWizard(new ExportWizardProvider(profile, version), i18n("modpack.wizard"));
public static void exportVersion(HMCLGameRepository repository, String version) {
Controllers.getDecorator().startWizard(new ExportWizardProvider(repository, version), i18n("modpack.wizard"));
}
public static void openFolder(Profile profile, String version) {
FXUtils.openFolder(profile.getRepository().getRunDirectory(version));
public static void openFolder(HMCLGameRepository repository, String version) {
FXUtils.openFolder(repository.getRunDirectory(version));
}
public static void installFromJson(Profile profile, Path file) {
public static void installFromJson(HMCLGameRepository repository, Path file) {
Version version;
try {
version = profile.getRepository().readVersionJson(file);
version = repository.readVersionJson(file);
} catch (Exception e) {
Controllers.dialog(i18n("install.new_game.malformed_json"), i18n("message.error"), MessageDialogPane.MessageType.ERROR);
return;
@@ -175,8 +177,7 @@ public final class Versions {
Controllers.prompt(i18n("version.manage.duplicate.prompt"), (result, handler) -> {
handler.resolve();
DefaultDependencyManager dependencyManager = profile.getDependency();
HMCLGameRepository repository = profile.getRepository();
DefaultDependencyManager dependencyManager = repository.getDependency();
Version newVersion = version.setId(result).setJar(result);
Controllers.taskDialog(
@@ -191,29 +192,29 @@ public final class Versions {
.thenRunAsync(repository::refreshVersions)
.whenComplete(Schedulers.javafx(), (exception) -> {
if (exception == null) {
Profiles.setSelectedInstance(profile, result);
repository.setSelectedInstance(result);
} else {
Controllers.dialog(
DownloadProviders.localizeErrorMessage(exception), i18n("install.failed"), MessageDialogPane.MessageType.ERROR);
}
}), i18n("install.new_game"), TaskCancellationAction.NORMAL);
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName)));
}, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.versionIdConflicts(newVersionName)));
}
public static void duplicateVersion(Profile profile, String version) {
public static void duplicateVersion(HMCLGameRepository repository, String version) {
Controllers.prompt(
new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> {
String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue();
boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue();
Task.runAsync(() -> profile.getRepository().duplicateVersion(version, newVersionName, copySaves))
.thenComposeAsync(profile.getRepository().refreshVersionsAsync())
Task.runAsync(() -> repository.duplicateVersion(version, newVersionName, copySaves))
.thenComposeAsync(repository.refreshVersionsAsync())
.whenComplete(Schedulers.javafx(), (result, exception) -> {
if (exception == null) {
handler.resolve();
} else {
handler.reject(StringUtils.getStackTrace(exception));
if (!profile.getRepository().versionIdConflicts(newVersionName)) {
profile.getRepository().removeVersionFromDisk(newVersionName);
if (!repository.versionIdConflicts(newVersionName)) {
repository.removeVersionFromDisk(newVersionName);
}
}
}).start();
@@ -221,35 +222,34 @@ public final class Versions {
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("version.manage.duplicate.confirm")))
.addQuestion(new PromptDialogPane.Builder.StringQuestion(null, version,
new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId),
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().versionIdConflicts(newVersionName))))
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.versionIdConflicts(newVersionName))))
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false)));
}
public static void updateVersion(Profile profile, String version) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(profile, version));
public static void updateVersion(HMCLGameRepository repository, String version) {
Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository, version));
}
public static void updateGameAssets(Profile profile, String version) {
TaskExecutor executor = new GameAssetDownloadTask(profile.getDependency(), profile.getRepository().getVersion(version), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)
public static void updateGameAssets(HMCLGameRepository repository, String version) {
TaskExecutor executor = new GameAssetDownloadTask(repository.getDependency(), repository.getVersion(version), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)
.executor();
Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL);
executor.start();
}
public static void cleanVersion(Profile profile, String id) {
public static void cleanVersion(HMCLGameRepository repository, String id) {
try {
profile.getRepository().clean(id);
repository.clean(id);
} catch (IOException e) {
LOG.warning("Unable to clean game directory", e);
}
}
@SafeVarargs
public static void generateLaunchScript(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(profile, id))
public static void generateLaunchScript(HMCLGameRepository repository, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(repository, id))
return;
ensureSelectedAccount(account -> {
GameRepository repository = profile.getRepository();
FileChooser chooser = new FileChooser();
if (Files.isDirectory(repository.getRunDirectory(id)))
chooser.setInitialDirectory(repository.getRunDirectory(id).toFile());
@@ -270,7 +270,7 @@ public final class Versions {
file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt);
}
LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
LauncherHelper launcherHelper = new LauncherHelper(repository, account, id);
for (Consumer<LauncherHelper> injecter : injecters) {
injecter.accept(launcherHelper);
}
@@ -295,11 +295,11 @@ public final class Versions {
}
@SafeVarargs
public static void launch(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(profile, id))
public static void launch(HMCLGameRepository repository, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(repository, id))
return;
ensureSelectedAccount(account -> {
LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
LauncherHelper launcherHelper = new LauncherHelper(repository, account, id);
for (Consumer<LauncherHelper> injecter : injecters) {
injecter.accept(launcherHelper);
}
@@ -307,22 +307,22 @@ public final class Versions {
});
}
public static void testGame(Profile profile, String id) {
launch(profile, id, LauncherHelper::setTestMode);
public static void testGame(HMCLGameRepository repository, String id) {
launch(repository, id, LauncherHelper::setTestMode);
}
public static void launchAndEnterWorld(Profile profile, String id, String worldFolderName) {
launch(profile, id, launcherHelper ->
public static void launchAndEnterWorld(HMCLGameRepository repository, String id, String worldFolderName) {
launch(repository, id, launcherHelper ->
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
}
public static void generateLaunchScriptForQuickEnterWorld(Profile profile, String id, String worldFolderName) {
generateLaunchScript(profile, id, launcherHelper ->
public static void generateLaunchScriptForQuickEnterWorld(HMCLGameRepository repository, String id, String worldFolderName) {
generateLaunchScript(repository, id, launcherHelper ->
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
}
private static boolean checkVersionForLaunching(Profile profile, String id) {
if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id)) {
private static boolean checkVersionForLaunching(HMCLGameRepository repository, String id) {
if (id == null || !repository.isLoaded() || !repository.hasVersion(id)) {
JFXButton gotoDownload = new JFXButton(i18n("version.empty.launch.goto_download"));
gotoDownload.getStyleClass().add("dialog-accept");
gotoDownload.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage()));
@@ -367,13 +367,13 @@ public final class Versions {
}
}
public static void modifyGlobalSettings(Profile profile) {
Controllers.getSettingsPage().showGameSettings(profile);
public static void modifyGlobalSettings(HMCLGameRepository repository) {
Controllers.getSettingsPage().showGameSettings(repository);
Controllers.navigate(Controllers.getSettingsPage());
}
public static void modifyGameSettings(Profile profile, String version) {
Controllers.getVersionPage().setVersion(version, profile);
public static void modifyGameSettings(HMCLGameRepository repository, String version) {
Controllers.getVersionPage().setVersion(version, repository);
Controllers.getVersionPage().showInstanceSettings();
// VersionPage.loadVersion will be invoked after navigation
Controllers.navigate(Controllers.getVersionPage());
@@ -35,8 +35,8 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.*;
@@ -61,12 +61,12 @@ import static org.jackhuang.hmcl.util.i18n.I18n.formatDateTime;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class WorldListPage extends ListPageBase<World> implements VersionPage.VersionLoadable {
public final class WorldListPage extends ListPageBase<World> implements VersionPage.GameInstanceLoadable {
private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false);
private Path savesDir;
private List<World> worlds;
private Profile profile;
private HMCLGameRepository repository;
private String instanceId;
private final BooleanProperty supportQuickPlay = new SimpleBooleanProperty(this, "supportQuickPlay", false);
@@ -86,10 +86,10 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
}
@Override
public void loadVersion(Profile profile, String id) {
this.profile = profile;
this.instanceId = id;
this.savesDir = profile.getRepository().getSavesDirectory(id);
public void loadInstance(HMCLGameRepository repository, String instanceId) {
this.repository = repository;
this.instanceId = instanceId;
this.savesDir = repository.getSavesDirectory(instanceId);
refresh();
}
@@ -99,7 +99,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
} else if (showAll.get()) {
getItems().setAll(worlds);
} else {
GameVersionNumber gameVersion = profile.getRepository().getGameVersion(instanceId).map(GameVersionNumber::asGameVersion).orElse(null);
GameVersionNumber gameVersion = repository.getGameVersion(instanceId).map(GameVersionNumber::asGameVersion).orElse(null);
getItems().setAll(worlds.stream()
.filter(world -> world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.toList());
@@ -107,7 +107,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
}
public void refresh() {
if (profile == null || instanceId == null)
if (repository == null || instanceId == null)
return;
int currentRefresh = ++refreshCount;
@@ -115,7 +115,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
setLoading(true);
Task.supplyAsync(Schedulers.io(), () -> {
// Ensure the game version number is parsed
profile.getRepository().getGameVersion(instanceId);
repository.getGameVersion(instanceId);
return World.getWorlds(savesDir);
}).whenComplete(Schedulers.javafx(), (result, exception) -> {
if (refreshCount != currentRefresh) {
@@ -123,7 +123,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
return;
}
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
Optional<String> gameVersion = repository.getGameVersion(instanceId);
supportQuickPlay.set(World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion)));
worlds = result;
@@ -177,7 +177,7 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
}
private void showManagePage(World world) {
Controllers.navigate(new WorldManagePage(world, profile, instanceId));
Controllers.navigate(new WorldManagePage(world, repository, instanceId));
}
public void export(World world) {
@@ -197,11 +197,11 @@ public final class WorldListPage extends ListPageBase<World> implements VersionP
}
public void launch(World world) {
Versions.launchAndEnterWorld(profile, instanceId, world.getFileName());
Versions.launchAndEnterWorld(repository, instanceId, world.getFileName());
}
public void generateLaunchScript(World world) {
Versions.generateLaunchScriptForQuickEnterWorld(profile, instanceId, world.getFileName());
Versions.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName());
}
public BooleanProperty showAllProperty() {
@@ -24,8 +24,8 @@ import javafx.geometry.Insets;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
@@ -53,7 +53,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private final World world;
private final Path backupsDir;
private final Profile profile;
private final HMCLGameRepository repository;
private final String instanceId;
private final boolean supportQuickPlay;
private FileChannel sessionLockChannel;
@@ -69,10 +69,10 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private final TabHeader.Tab<WorldBackupsPage> worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage");
private final TabHeader.Tab<DataPackListPage> dataPackTab = new TabHeader.Tab<>("dataPackListPage");
public WorldManagePage(World world, Profile profile, String instanceId) {
public WorldManagePage(World world, HMCLGameRepository repository, String instanceId) {
this.world = world;
this.backupsDir = profile.getRepository().getBackupsDirectory(instanceId);
this.profile = profile;
this.backupsDir = repository.getBackupsDirectory(instanceId);
this.repository = repository;
this.instanceId = instanceId;
updateSessionLockChannel();
@@ -90,7 +90,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
this.state = new SimpleObjectProperty<>(new State(i18n("world.manage.title", StringUtils.parseColorEscapes(world.getWorldName())), null, true, true, true));
Optional<String> gameVersion = profile.getRepository().getGameVersion(instanceId);
Optional<String> gameVersion = repository.getGameVersion(instanceId);
supportQuickPlay = World.supportQuickPlay(GameVersionNumber.asGameVersion(gameVersion));
this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
@@ -150,11 +150,11 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
public void launch() {
fireEvent(new PageCloseEvent());
Versions.launchAndEnterWorld(profile, instanceId, world.getFileName());
Versions.launchAndEnterWorld(repository, instanceId, world.getFileName());
}
public void generateLaunchScript() {
Versions.generateLaunchScriptForQuickEnterWorld(profile, instanceId, world.getFileName());
Versions.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName());
}
@Override
+6 -6
View File
@@ -210,28 +210,28 @@
-fx-padding: 0 0 0 0;
}
.profile-list-item > .rippler-container > BorderPane > .two-line-list-item > .first-line > .title {
.game-directory-list-item > .rippler-container > BorderPane > .two-line-list-item > .first-line > .title {
-fx-font-size: 13;
}
.profile-list-item > .rippler-container > BorderPane > .two-line-list-item > HBox > .subtitle {
.game-directory-list-item > .rippler-container > BorderPane > .two-line-list-item > HBox > .subtitle {
-fx-font-size: 10;
}
.profile-list-item > .rippler-container > BorderPane {
.game-directory-list-item > .rippler-container > BorderPane {
-fx-padding: 8 0 8 16;
}
.profile-list-item:selected > .rippler-container > BorderPane {
.game-directory-list-item:selected > .rippler-container > BorderPane {
-fx-background-color: -monet-secondary-container-transparent-50;
}
.profile-list-item:selected > .rippler-container > BorderPane > .two-line-list-item > .first-line > .title {
.game-directory-list-item:selected > .rippler-container > BorderPane > .two-line-list-item > .first-line > .title {
-fx-text-fill: -monet-on-secondary-container;
-fx-font-weight: bold;
}
.profile-list-item:selected .svg {
.game-directory-list-item:selected .svg {
-fx-fill: -monet-on-secondary-container;
}
@@ -742,6 +742,19 @@ game.crash.title=Game Crashed
game.directory=Game Path
game.version=Game Instance
game_directory=Game Directories
game_directory.already_exists=This name already exists. Please use a different name.
game_directory.default=Current
game_directory.home=Minecraft Launcher
game_directory.instance_directory=Game Directory
game_directory.instance_directory.choose=Choose game directory
game_directory.manage=Instance Directory List
game_directory.name=Name
game_directory.new=New Directory
game_directory.title=Game Directories
game_directory.selected=Selected
game_directory.use_relative_path=Use a relative path for the game directory if possible
help=Help
help.doc=Hello Minecraft! Launcher Documentation
help.detail=For data pack and modpack makers.
@@ -1264,19 +1277,6 @@ world.name=World Name
world.name.enter=Enter the world name
world.show_all=Show All
profile=Game Directories
profile.already_exists=This name already exists. Please use a different name.
profile.default=Current
profile.home=Minecraft Launcher
profile.instance_directory=Game Directory
profile.instance_directory.choose=Choose game directory
profile.manage=Instance Directory List
profile.name=Name
profile.new=New Directory
profile.title=Game Directories
profile.selected=Selected
profile.use_relative_path=Use a relative path for the game directory if possible
repositories.custom=Custom Maven Repository (%s)
repositories.maven_central=Universal (Maven Central)
repositories.tencentcloud_mirror=Chinese Mainland Mirror (Tencent Cloud Maven Repository)
@@ -706,6 +706,19 @@ game.crash.title=تعطّلت اللعبة
game.directory=مسار اللعبة
game.version=نسخة اللعبة
game_directory=مجلدات اللعبة
game_directory.already_exists=هذا الاسم موجود مسبقاً. يُرجى استخدام اسم مختلف.
game_directory.default=الحالي
game_directory.home=مشغّل Minecraft
game_directory.instance_directory=مجلد اللعبة
game_directory.instance_directory.choose=اختر مجلد اللعبة
game_directory.manage=قائمة مجلدات النسخ
game_directory.name=الاسم
game_directory.new=مجلد جديد
game_directory.title=مجلدات اللعبة
game_directory.selected=محدد
game_directory.use_relative_path=استخدام مسار نسبي لمجلد اللعبة إن أمكن
help=مساعدة
help.doc=توثيق Hello Minecraft! Launcher
help.detail=لصانعي حزم البيانات وحزم المودات.
@@ -1208,19 +1221,6 @@ world.name=اسم العالم
world.name.enter=أدخل اسم العالم
world.show_all=عرض الكل
profile=مجلدات اللعبة
profile.already_exists=هذا الاسم موجود مسبقاً. يُرجى استخدام اسم مختلف.
profile.default=الحالي
profile.home=مشغّل Minecraft
profile.instance_directory=مجلد اللعبة
profile.instance_directory.choose=اختر مجلد اللعبة
profile.manage=قائمة مجلدات النسخ
profile.name=الاسم
profile.new=مجلد جديد
profile.title=مجلدات اللعبة
profile.selected=محدد
profile.use_relative_path=استخدام مسار نسبي لمجلد اللعبة إن أمكن
repositories.custom=مستودع Maven مخصص (%s)
repositories.maven_central=عالمي (Maven Central)
repositories.tencentcloud_mirror=مرآة البر الرئيسي الصيني (مستودع Tencent Cloud Maven)
@@ -672,6 +672,19 @@ game.crash.title=Juego colgado
game.directory=Ruta del juego
game.version=Instancia del juego
game_directory=Directorios del juego
game_directory.already_exists=Este nombre ya existe, por favor, utilice un nombre diferente.
game_directory.default=Directorio actual
game_directory.home=Minecraft Launcher
game_directory.instance_directory=Directorio ded juego
game_directory.instance_directory.choose=Elegir directorio de juegos
game_directory.manage=Lista de directorios de instancia
game_directory.name=Nombre
game_directory.new=Nuevo directorio
game_directory.title=Directorios del juego
game_directory.selected=Seleccionado
game_directory.use_relative_path=Utilizar ruta relativa para la ruta del juego si es posible
help=Ayuda
help.doc=Documentación de Hello Minecraft! Launcher
help.detail=Para los creadores de paquetes de datos y mods.
@@ -1131,19 +1144,6 @@ world.name=Nombre del mundo
world.name.enter=Introducir el nombre del mundo
world.show_all=Mostrar todo
profile=Directorios del juego
profile.already_exists=Este nombre ya existe, por favor, utilice un nombre diferente.
profile.default=Directorio actual
profile.home=Minecraft Launcher
profile.instance_directory=Directorio ded juego
profile.instance_directory.choose=Elegir directorio de juegos
profile.manage=Lista de directorios de instancia
profile.name=Nombre
profile.new=Nuevo directorio
profile.title=Directorios del juego
profile.selected=Seleccionado
profile.use_relative_path=Utilizar ruta relativa para la ruta del juego si es posible
repositories.custom=Repositorio Maven personalizado (%s)
repositories.maven_central=Universal (Maven Central)
repositories.tencentcloud_mirror=Espejo de China continental (Repositorio Maven Tencent Cloud)
@@ -414,6 +414,19 @@ game.crash.title=ゲームがクラッシュしました
game.directory=ゲームディレクトリ
game.version=ゲームバージョン
game_directory=ゲームディレクトリ
game_directory.already_exists=この名前はすでに存在します。別の名前を使用してください。
game_directory.default=現在のディレクトリ
game_directory.home=標準ディレクトリ
game_directory.instance_directory=ゲームディレクトリ
game_directory.instance_directory.choose=ゲームディレクトリを選択してください
game_directory.manage=ゲームディレクトリリスト
game_directory.name=名前
game_directory.new=新規作成
game_directory.title=ゲームディレクトリ
game_directory.selected=選択済み
game_directory.use_relative_path=可能であれば、ゲームディレクトリの相対パスを使用します
help=ヘルプ
help.doc=ドキュメント
help.detail=データパック、modpackなどのメーカー向け。
@@ -703,19 +716,6 @@ world.name=マップ名
world.name.enter=マップ名を入力してください
world.show_all=すべて表示
profile=ゲームディレクトリ
profile.already_exists=この名前はすでに存在します。別の名前を使用してください。
profile.default=現在のディレクトリ
profile.home=標準ディレクトリ
profile.instance_directory=ゲームディレクトリ
profile.instance_directory.choose=ゲームディレクトリを選択してください
profile.manage=ゲームディレクトリリスト
profile.name=名前
profile.new=新規作成
profile.title=ゲームディレクトリ
profile.selected=選択済み
profile.use_relative_path=可能であれば、ゲームディレクトリの相対パスを使用します
repositories.custom=カスタムMavenリポジトリ(%s)
repositories.maven_central=Universal(Maven Central)
repositories.tencentcloud_mirror=中国本土(Tencent Cloud Mavenリポジトリ)
@@ -488,6 +488,19 @@ game.crash.title=不期而退
game.directory=戲案夾之径
game.version=戲例
game_directory=戲案夾
game_directory.already_exists=名既存
game_directory.default=當案夾
game_directory.home=官之啟者置設文之案夾
game_directory.instance_directory=戲案夾之徑
game_directory.instance_directory.choose=擇戲案夾之徑
game_directory.manage=戲案夾列
game_directory.name=名
game_directory.new=另增戲案夾
game_directory.title=戲案夾
game_directory.selected=既擇
game_directory.use_relative_path=苟能是,戲案夾用相對之徑
help=助
help.detail=所以閲錄囊、改囊集之司南與他者
help.doc=Hello Minecraft! Launcher 之案文
@@ -946,19 +959,6 @@ world.name=生界之名
world.name.enter=書生界之名
world.show_all=全顯
profile=戲案夾
profile.already_exists=名既存
profile.default=當案夾
profile.home=官之啟者置設文之案夾
profile.instance_directory=戲案夾之徑
profile.instance_directory.choose=擇戲案夾之徑
profile.manage=戲案夾列
profile.name=名
profile.new=另增戲案夾
profile.title=戲案夾
profile.selected=既擇
profile.use_relative_path=苟能是,戲案夾用相對之徑
repositories.custom=自定 Maven 倉 (%s)
repositories.maven_central=天下之倉 (Maven Central)
repositories.tencentcloud_mirror=本朝 (騰訊云轉寫 Maven 倉儲)
@@ -664,6 +664,19 @@ game.crash.title=Игра отказала!
game.directory=Путь игры
game.version=Сборка
game_directory=Папки с играми
game_directory.already_exists=Имя уже существует, используйте другое имя.
game_directory.default=Текущая папка
game_directory.home=Лаунчер Minecraft
game_directory.instance_directory=Папка с игрой
game_directory.instance_directory.choose=Выберите папку с игрой
game_directory.manage=Список папок для сборки
game_directory.name=Имя
game_directory.new=Новая папка
game_directory.title=Папки с играми
game_directory.selected=Выбранный
game_directory.use_relative_path=По возможности используйте относительный путь для игры
help=Справка
help.doc=Документация лаунчера
help.detail=Для создателей набора данных и модпака.
@@ -1123,19 +1136,6 @@ world.name=Название мира
world.name.enter=Введите название мира
world.show_all=Показать все
profile=Папки с играми
profile.already_exists=Имя уже существует, используйте другое имя.
profile.default=Текущая папка
profile.home=Лаунчер Minecraft
profile.instance_directory=Папка с игрой
profile.instance_directory.choose=Выберите папку с игрой
profile.manage=Список папок для сборки
profile.name=Имя
profile.new=Новая папка
profile.title=Папки с играми
profile.selected=Выбранный
profile.use_relative_path=По возможности используйте относительный путь для игры
repositories.custom=Пользовательский репозиторий Maven (%s)
repositories.maven_central=Универсальный (Maven Central)
repositories.tencentcloud_mirror=Зеркало Mainland China (Репозиторий Tencent Cloud Maven)
@@ -615,6 +615,19 @@ game.crash.title=Гра зазнала збою
game.directory=Шлях до гри
game.version=Екземпляр гри
game_directory=Ігрові каталоги
game_directory.already_exists=Це ім'я вже існує. Використовуйте інше ім'я.
game_directory.default=Поточний
game_directory.home=Лаунчер Minecraft
game_directory.instance_directory=Каталог гри
game_directory.instance_directory.choose=Вибрати каталог гри
game_directory.manage=Список каталогів екземплярів
game_directory.name=Назва
game_directory.new=Новий каталог
game_directory.title=Ігрові каталоги
game_directory.selected=Вибраний
game_directory.use_relative_path=Використовувати відносний шлях для каталогу гри, якщо можливо
help=Допомога
help.doc=Документація Hello Minecraft! Лаунчера
help.detail=Для творців datapack та модпаків.
@@ -1072,19 +1085,6 @@ world.name=Назва світу
world.name.enter=Введіть назву світу
world.show_all=Показати все
profile=Ігрові каталоги
profile.already_exists=Це ім'я вже існує. Використовуйте інше ім'я.
profile.default=Поточний
profile.home=Лаунчер Minecraft
profile.instance_directory=Каталог гри
profile.instance_directory.choose=Вибрати каталог гри
profile.manage=Список каталогів екземплярів
profile.name=Назва
profile.new=Новий каталог
profile.title=Ігрові каталоги
profile.selected=Вибраний
profile.use_relative_path=Використовувати відносний шлях для каталогу гри, якщо можливо
repositories.custom=Власне сховище Maven (%s)
repositories.maven_central=Універсальне (Maven Central)
repositories.tencentcloud_mirror=Дзеркало материкового Китаю (Сховище Maven Tencent Cloud)
@@ -549,6 +549,19 @@ game.crash.title=遊戲意外退出
game.directory=遊戲目錄路徑
game.version=遊戲實例
game_directory=遊戲目錄
game_directory.already_exists=該名稱已存在
game_directory.default=目前目錄
game_directory.home=官方啟動器目錄
game_directory.instance_directory=遊戲目錄路徑
game_directory.instance_directory.choose=選取遊戲目錄路徑
game_directory.manage=遊戲目錄清單
game_directory.name=名稱
game_directory.new=建立新目錄
game_directory.title=遊戲目錄
game_directory.selected=已選取
game_directory.use_relative_path=如可行,則對於遊戲目錄使用相對路徑
help=說明
help.doc=Hello Minecraft! Launcher 說明文件
help.detail=可查閱資料包、模組包製作教學等內容
@@ -1061,19 +1074,6 @@ world.name=世界名稱
world.name.enter=輸入世界名稱
world.show_all=全部顯示
profile=遊戲目錄
profile.already_exists=該名稱已存在
profile.default=目前目錄
profile.home=官方啟動器目錄
profile.instance_directory=遊戲目錄路徑
profile.instance_directory.choose=選取遊戲目錄路徑
profile.manage=遊戲目錄清單
profile.name=名稱
profile.new=建立新目錄
profile.title=遊戲目錄
profile.selected=已選取
profile.use_relative_path=如可行,則對於遊戲目錄使用相對路徑
repositories.custom=自訂 Maven 儲存庫 (%s)
repositories.maven_central=全球 (Maven Central)
repositories.tencentcloud_mirror=中國大陸 (騰訊雲 Maven 儲存庫)
@@ -554,6 +554,19 @@ game.crash.title=游戏意外退出
game.directory=游戏文件夹路径
game.version=游戏实例
game_directory=游戏文件夹
game_directory.already_exists=该名称已存在
game_directory.default=当前文件夹
game_directory.home=官方启动器文件夹
game_directory.instance_directory=游戏文件夹路径
game_directory.instance_directory.choose=选择游戏文件夹路径
game_directory.manage=游戏文件夹列表
game_directory.name=名称
game_directory.new=添加游戏文件夹
game_directory.title=游戏文件夹
game_directory.selected=已选中
game_directory.use_relative_path=若可能,游戏文件夹使用相对路径
help=帮助
help.doc=Hello Minecraft! Launcher 帮助文档
help.detail=可查阅数据包、整合包制作指南等内容
@@ -1066,19 +1079,6 @@ world.name=世界名称
world.name.enter=输入世界名称
world.show_all=显示全部
profile=游戏文件夹
profile.already_exists=该名称已存在
profile.default=当前文件夹
profile.home=官方启动器文件夹
profile.instance_directory=游戏文件夹路径
profile.instance_directory.choose=选择游戏文件夹路径
profile.manage=游戏文件夹列表
profile.name=名称
profile.new=添加游戏文件夹
profile.title=游戏文件夹
profile.selected=已选中
profile.use_relative_path=若可能,游戏文件夹使用相对路径
repositories.custom=自定义 Maven 仓库 (%s)
repositories.maven_central=全球 (Maven Central)
repositories.tencentcloud_mirror=中国大陆 (腾讯云镜像源 Maven 仓库)
@@ -25,6 +25,7 @@ import com.google.gson.JsonParser;
import javafx.beans.property.ObjectProperty;
import javafx.collections.ObservableList;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.PortablePath;
import org.jackhuang.hmcl.util.gson.JsonSchema;
@@ -39,6 +40,7 @@ import java.nio.file.FileSystem;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.jackhuang.hmcl.setting.SettingsManager.settings;
@@ -67,7 +69,7 @@ public final class GameDirectoriesTest {
assertFalse(settings.has("configurations"));
assertEquals(1, gameDirectories.getGameDirectories().size());
assertEquals(id, gameDirectories.getGameDirectories().get(0).getId());
assertEquals("Dev", Profiles.getProfileCustomName(gameDirectories.getGameDirectories().get(0)));
assertEquals("Dev", GameDirectoryManager.getGameDirectoryCustomName(gameDirectories.getGameDirectories().get(0)));
assertEquals(".minecraft", gameDirectories.getGameDirectories().get(0).getPath().getPath());
assertNull(gameDirectories.getGameDirectories().get(0).getLegacyGameSettings());
}
@@ -93,24 +95,24 @@ public final class GameDirectoriesTest {
GameDirectories gameDirectories = Objects.requireNonNull(LegacyConfigMigrator.extractGameDirectoriesFromConfigJson(settings));
GameDirectoryID defaultProfileId = LegacyConfigMigrator.getLegacyProfileID("Default");
GameDirectoryID homeProfileId = LegacyConfigMigrator.getLegacyProfileID("Home");
Profile defaultProfile = gameDirectories.getGameDirectories().stream()
.filter(profile -> defaultProfileId.equals(profile.getId()))
GameDirectoryID defaultGameDirectoryId = LegacyConfigMigrator.getLegacyProfileID("Default");
GameDirectoryID homeGameDirectoryId = LegacyConfigMigrator.getLegacyProfileID("Home");
GameDirectory defaultGameDirectory = gameDirectories.getGameDirectories().stream()
.filter(gameDirectory -> defaultGameDirectoryId.equals(gameDirectory.getId()))
.findFirst()
.orElseThrow();
Profile homeProfile = gameDirectories.getGameDirectories().stream()
.filter(profile -> homeProfileId.equals(profile.getId()))
GameDirectory homeGameDirectory = gameDirectories.getGameDirectories().stream()
.filter(gameDirectory -> homeGameDirectoryId.equals(gameDirectory.getId()))
.findFirst()
.orElseThrow();
Profile devProfile = gameDirectories.getGameDirectories().stream()
.filter(profile -> "Dev".equals(Profiles.getProfileCustomName(profile)))
GameDirectory devGameDirectory = gameDirectories.getGameDirectories().stream()
.filter(gameDirectory -> "Dev".equals(GameDirectoryManager.getGameDirectoryCustomName(gameDirectory)))
.findFirst()
.orElseThrow();
assertNull(defaultProfile.getName());
assertNull(homeProfile.getName());
assertEquals("Dev", Profiles.getProfileCustomName(devProfile));
assertNull(defaultGameDirectory.getName());
assertNull(homeGameDirectory.getName());
assertEquals("Dev", GameDirectoryManager.getGameDirectoryCustomName(devGameDirectory));
}
/// Tests migrating legacy selected version fields into the main config.
@@ -168,14 +170,14 @@ public final class GameDirectoriesTest {
assertNull(config.selectedGameDirectoryProperty().get());
}
/// Tests that profiles store their directory as a portable path.
/// Tests that game directories store their path as a portable path.
@Test
public void storesProfilePath() {
public void storesGameDirectoryPath() {
GameDirectoryID id = GameDirectoryID.parse("game-directory:123e4567-e89b-12d3-a456-426614174000");
Profile profile = new Profile(id, LocalizedText.plain("Dev"), PortablePath.of("versions\\Dev"));
GameDirectory gameDirectory = new GameDirectory(id, LocalizedText.plain("Dev"), PortablePath.of("versions\\Dev"));
JsonObject serialized = JsonUtils.GSON.toJsonTree(profile, Profile.class).getAsJsonObject();
Profile deserialized = Objects.requireNonNull(JsonUtils.GSON.fromJson(serialized, Profile.class));
JsonObject serialized = JsonUtils.GSON.toJsonTree(gameDirectory, GameDirectory.class).getAsJsonObject();
GameDirectory deserialized = Objects.requireNonNull(JsonUtils.GSON.fromJson(serialized, GameDirectory.class));
assertEquals("versions/Dev", serialized.get("path").getAsString());
assertEquals("Dev", serialized.get("name").getAsString());
@@ -185,202 +187,228 @@ public final class GameDirectoriesTest {
assertFalse(deserialized.getPath().isAbsolute());
}
/// Tests that profiles preserve migrated legacy game settings IDs.
/// Tests that game directories preserve migrated legacy game settings IDs.
@Test
public void storesLegacyGameSettingsId() {
GameDirectoryID id = GameDirectoryID.parse("game-directory:123e4567-e89b-12d3-a456-426614174000");
GameSettingsPresetID legacyGameSettings =
GameSettingsPresetID.parse("game-settings-preset:123e4567-e89b-12d3-a456-426614174001");
Profile profile = new Profile(
GameDirectory gameDirectory = new GameDirectory(
id,
LocalizedText.plain("Dev"),
PortablePath.of("versions\\Dev"),
legacyGameSettings);
JsonObject serialized = JsonUtils.GSON.toJsonTree(profile, Profile.class).getAsJsonObject();
Profile deserialized = Objects.requireNonNull(JsonUtils.GSON.fromJson(serialized, Profile.class));
JsonObject serialized = JsonUtils.GSON.toJsonTree(gameDirectory, GameDirectory.class).getAsJsonObject();
GameDirectory deserialized = Objects.requireNonNull(JsonUtils.GSON.fromJson(serialized, GameDirectory.class));
assertEquals(legacyGameSettings.toString(), serialized.get("legacyGameSettings").getAsString());
assertEquals(legacyGameSettings, deserialized.getLegacyGameSettings());
}
/// Tests that the merged profile view is read-only and does not own the backing profile data.
/// Tests that the merged game directory view is read-only and does not own the backing store data.
@Test
public void keepsShadowedUserProfileInBackingStore() throws ReflectiveOperationException {
public void keepsShadowedUserGameDirectoryInBackingStore() throws ReflectiveOperationException {
GameDirectoryID id = GameDirectoryID.parse("game-directory:123e4567-e89b-12d3-a456-426614174000");
Profile userProfile = new Profile(id, LocalizedText.plain("User"), PortablePath.of("user/Dev"));
Profile localProfile = new Profile(id, LocalizedText.plain("Local"), PortablePath.of("local/Dev"));
Profile addedProfile = new Profile(
GameDirectory userGameDirectory = new GameDirectory(id, LocalizedText.plain("User"), PortablePath.of("user/Dev"));
GameDirectory localGameDirectory = new GameDirectory(id, LocalizedText.plain("Local"), PortablePath.of("local/Dev"));
GameDirectory addedGameDirectory = new GameDirectory(
GameDirectoryID.parse("game-directory:123e4567-e89b-12d3-a456-426614174001"),
LocalizedText.plain("Added"),
PortablePath.of("local/Added"));
GameDirectories userDirectories = new GameDirectories();
userDirectories.getGameDirectories().add(userProfile);
userDirectories.getGameDirectories().add(userGameDirectory);
userDirectories.setUserFile(true);
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(localProfile);
localDirectories.getGameDirectories().add(localGameDirectory);
localDirectories.setUserFile(false);
try (ProfileEnvironment ignored = new ProfileEnvironment(localDirectories, userDirectories)) {
Profiles.init();
ObservableList<Profile> gameDirectories = Profiles.getProfiles();
try (GameDirectoryEnvironment ignored = new GameDirectoryEnvironment(localDirectories, userDirectories)) {
GameDirectoryManager.init();
ObservableList<GameDirectory> gameDirectories = GameDirectoryManager.getGameDirectories();
assertEquals(List.of(localProfile), gameDirectories);
assertThrows(UnsupportedOperationException.class, () -> gameDirectories.add(addedProfile));
assertSame(localProfile, Profiles.getSelectedProfile());
assertEquals(localProfile.getId(), settings().selectedGameDirectoryProperty().get());
assertEquals(List.of(userProfile), userDirectories.getGameDirectories());
assertEquals(List.of(localProfile), localDirectories.getGameDirectories());
assertEquals(List.of(localGameDirectory), gameDirectories);
assertThrows(UnsupportedOperationException.class, () -> gameDirectories.add(addedGameDirectory));
assertSame(localGameDirectory, GameDirectoryManager.getSelectedGameDirectory());
assertEquals(localGameDirectory.getId(), settings().selectedGameDirectoryProperty().get());
assertEquals(List.of(userGameDirectory), userDirectories.getGameDirectories());
assertEquals(List.of(localGameDirectory), localDirectories.getGameDirectories());
Profiles.removeProfile(localProfile);
assertEquals(List.of(userProfile), Profiles.getProfiles());
assertSame(userProfile, Profiles.getSelectedProfile());
assertEquals(userProfile.getId(), settings().selectedGameDirectoryProperty().get());
assertEquals(List.of(userProfile), userDirectories.getGameDirectories());
GameDirectoryManager.removeGameDirectory(localGameDirectory);
assertEquals(List.of(userGameDirectory), GameDirectoryManager.getGameDirectories());
assertSame(userGameDirectory, GameDirectoryManager.getSelectedGameDirectory());
assertEquals(userGameDirectory.getId(), settings().selectedGameDirectoryProperty().get());
assertEquals(List.of(userGameDirectory), userDirectories.getGameDirectories());
assertTrue(localDirectories.getGameDirectories().isEmpty());
Profiles.addLocalProfile(addedProfile);
assertEquals(List.of(addedProfile, userProfile), Profiles.getProfiles());
assertEquals(List.of(addedProfile), localDirectories.getGameDirectories());
assertSame(userProfile, Profiles.getSelectedProfile());
GameDirectoryManager.addLocalGameDirectory(addedGameDirectory);
assertEquals(List.of(addedGameDirectory, userGameDirectory), GameDirectoryManager.getGameDirectories());
assertEquals(List.of(addedGameDirectory), localDirectories.getGameDirectories());
assertSame(userGameDirectory, GameDirectoryManager.getSelectedGameDirectory());
Profiles.setSelectedProfile(addedProfile);
assertSame(addedProfile, Profiles.getSelectedProfile());
assertEquals(addedProfile.getId(), settings().selectedGameDirectoryProperty().get());
GameDirectoryManager.setSelectedGameDirectory(addedGameDirectory);
assertSame(addedGameDirectory, GameDirectoryManager.getSelectedGameDirectory());
assertEquals(addedGameDirectory.getId(), settings().selectedGameDirectoryProperty().get());
}
}
/// Tests that editing a profile moves it to the store selected by its new path type.
/// Tests that editing a game directory moves it to the store selected by its new path type.
@Test
public void movesProfileBetweenStoresWhenPathTypeChanges() throws ReflectiveOperationException {
public void movesGameDirectoryBetweenStoresWhenPathTypeChanges() throws ReflectiveOperationException {
GameDirectoryID id = GameDirectoryID.parse("game-directory:123e4567-e89b-12d3-a456-426614174000");
Profile profile = new Profile(id, LocalizedText.plain("Local"), PortablePath.of("local/Dev"));
GameDirectory gameDirectory = new GameDirectory(id, LocalizedText.plain("Local"), PortablePath.of("local/Dev"));
GameDirectories userDirectories = new GameDirectories();
userDirectories.setUserFile(true);
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(profile);
localDirectories.getGameDirectories().add(gameDirectory);
localDirectories.setUserFile(false);
try (ProfileEnvironment ignored = new ProfileEnvironment(localDirectories, userDirectories)) {
Profiles.init();
try (GameDirectoryEnvironment ignored = new GameDirectoryEnvironment(localDirectories, userDirectories)) {
GameDirectoryManager.init();
PortablePath absolutePath = PortablePath.of("/workspace/Dev");
Profiles.updateProfile(profile, LocalizedText.plain("Moved"), absolutePath);
GameDirectoryManager.updateGameDirectory(gameDirectory, LocalizedText.plain("Moved"), absolutePath);
assertTrue(localDirectories.getGameDirectories().isEmpty());
assertEquals(List.of(profile), userDirectories.getGameDirectories());
assertEquals(List.of(profile), Profiles.getProfiles());
assertSame(profile, Profiles.getSelectedProfile());
assertEquals(absolutePath.getPath(), profile.getPath().getPath());
assertEquals(absolutePath.isAbsolute(), profile.getPath().isAbsolute());
assertEquals("Moved", Profiles.getProfileCustomName(profile));
assertEquals(List.of(gameDirectory), userDirectories.getGameDirectories());
assertEquals(List.of(gameDirectory), GameDirectoryManager.getGameDirectories());
assertSame(gameDirectory, GameDirectoryManager.getSelectedGameDirectory());
assertEquals(absolutePath.getPath(), gameDirectory.getPath().getPath());
assertEquals(absolutePath.isAbsolute(), gameDirectory.getPath().isAbsolute());
assertEquals("Moved", GameDirectoryManager.getGameDirectoryCustomName(gameDirectory));
PortablePath relativePath = PortablePath.of("local/Dev");
Profiles.updateProfile(profile, LocalizedText.plain("Back"), relativePath);
GameDirectoryManager.updateGameDirectory(gameDirectory, LocalizedText.plain("Back"), relativePath);
assertEquals(List.of(profile), localDirectories.getGameDirectories());
assertEquals(List.of(gameDirectory), localDirectories.getGameDirectories());
assertTrue(userDirectories.getGameDirectories().isEmpty());
assertEquals(List.of(profile), Profiles.getProfiles());
assertSame(profile, Profiles.getSelectedProfile());
assertEquals(relativePath.getPath(), profile.getPath().getPath());
assertEquals(relativePath.isAbsolute(), profile.getPath().isAbsolute());
assertEquals("Back", Profiles.getProfileCustomName(profile));
assertEquals(List.of(gameDirectory), GameDirectoryManager.getGameDirectories());
assertSame(gameDirectory, GameDirectoryManager.getSelectedGameDirectory());
assertEquals(relativePath.getPath(), gameDirectory.getPath().getPath());
assertEquals(relativePath.isAbsolute(), gameDirectory.getPath().isAbsolute());
assertEquals("Back", GameDirectoryManager.getGameDirectoryCustomName(gameDirectory));
}
}
/// Tests that profile mutations reject writes to read-only source and target stores.
/// Tests that game directory mutations reject writes to read-only source and target stores.
@Test
public void rejectsProfileMutationsWithReadOnlyStores() throws ReflectiveOperationException {
public void rejectsGameDirectoryMutationsWithReadOnlyStores() throws ReflectiveOperationException {
GameDirectoryID id = GameDirectoryID.parse("game-directory:123e4567-e89b-12d3-a456-426614174000");
Profile profile = new Profile(id, LocalizedText.plain("Local"), PortablePath.of("local/Dev"));
GameDirectory gameDirectory = new GameDirectory(id, LocalizedText.plain("Local"), PortablePath.of("local/Dev"));
GameDirectories userDirectories = new GameDirectories();
userDirectories.setUserFile(true);
GameDirectories localDirectories = new GameDirectories();
localDirectories.getGameDirectories().add(profile);
localDirectories.getGameDirectories().add(gameDirectory);
localDirectories.setUserFile(false);
try (ProfileEnvironment environment = new ProfileEnvironment(localDirectories, userDirectories)) {
try (GameDirectoryEnvironment environment = new GameDirectoryEnvironment(localDirectories, userDirectories)) {
environment.setLocalGameDirectoriesAccess(SettingFileAccess.READ_ONLY);
Profiles.init();
GameDirectoryManager.init();
PortablePath newPath = PortablePath.of("local/Renamed");
assertFalse(Profiles.canUpdateProfile(profile, newPath));
assertFalse(GameDirectoryManager.canUpdateGameDirectory(gameDirectory, newPath));
assertThrows(IllegalStateException.class,
() -> Profiles.updateProfile(profile, LocalizedText.plain("Renamed"), newPath));
assertFalse(Profiles.canRemoveProfile(profile));
assertThrows(IllegalStateException.class, () -> Profiles.removeProfile(profile));
assertEquals(List.of(profile), localDirectories.getGameDirectories());
() -> GameDirectoryManager.updateGameDirectory(gameDirectory, LocalizedText.plain("Renamed"), newPath));
assertFalse(GameDirectoryManager.canRemoveGameDirectory(gameDirectory));
assertThrows(IllegalStateException.class, () -> GameDirectoryManager.removeGameDirectory(gameDirectory));
assertEquals(List.of(gameDirectory), localDirectories.getGameDirectories());
assertTrue(userDirectories.getGameDirectories().isEmpty());
assertEquals("local/Dev", profile.getPath().getPath());
assertFalse(profile.getPath().isAbsolute());
assertEquals("Local", Profiles.getProfileCustomName(profile));
assertEquals("local/Dev", gameDirectory.getPath().getPath());
assertFalse(gameDirectory.getPath().isAbsolute());
assertEquals("Local", GameDirectoryManager.getGameDirectoryCustomName(gameDirectory));
}
Profile targetProfile = new Profile(id, LocalizedText.plain("Local"), PortablePath.of("local/Dev"));
GameDirectory targetGameDirectory = new GameDirectory(id, LocalizedText.plain("Local"), PortablePath.of("local/Dev"));
GameDirectories targetUserDirectories = new GameDirectories();
targetUserDirectories.setUserFile(true);
GameDirectories targetLocalDirectories = new GameDirectories();
targetLocalDirectories.getGameDirectories().add(targetProfile);
targetLocalDirectories.getGameDirectories().add(targetGameDirectory);
targetLocalDirectories.setUserFile(false);
try (ProfileEnvironment environment = new ProfileEnvironment(targetLocalDirectories, targetUserDirectories)) {
try (GameDirectoryEnvironment environment = new GameDirectoryEnvironment(targetLocalDirectories, targetUserDirectories)) {
environment.setUserGameDirectoriesAccess(SettingFileAccess.READ_ONLY);
Profiles.init();
GameDirectoryManager.init();
PortablePath absolutePath = PortablePath.of("/workspace/Dev");
assertFalse(Profiles.canUpdateProfile(targetProfile, absolutePath));
assertFalse(GameDirectoryManager.canUpdateGameDirectory(targetGameDirectory, absolutePath));
assertThrows(IllegalStateException.class,
() -> Profiles.updateProfile(targetProfile, LocalizedText.plain("Moved"), absolutePath));
assertEquals(List.of(targetProfile), targetLocalDirectories.getGameDirectories());
() -> GameDirectoryManager.updateGameDirectory(targetGameDirectory, LocalizedText.plain("Moved"), absolutePath));
assertEquals(List.of(targetGameDirectory), targetLocalDirectories.getGameDirectories());
assertTrue(targetUserDirectories.getGameDirectories().isEmpty());
assertEquals("local/Dev", targetProfile.getPath().getPath());
assertFalse(targetProfile.getPath().isAbsolute());
assertEquals("Local", Profiles.getProfileCustomName(targetProfile));
assertEquals("local/Dev", targetGameDirectory.getPath().getPath());
assertFalse(targetGameDirectory.getPath().isAbsolute());
assertEquals("Local", GameDirectoryManager.getGameDirectoryCustomName(targetGameDirectory));
}
}
/// Tests that default profiles are created when both loaded stores are empty.
/// Tests that default game directories are created when both loaded stores are empty.
@Test
public void createsDefaultProfilesWhenMergedProfilesAreEmpty() throws ReflectiveOperationException {
public void createsDefaultGameDirectoriesWhenMergedGameDirectoriesAreEmpty() throws ReflectiveOperationException {
GameDirectories userDirectories = new GameDirectories();
userDirectories.setUserFile(true);
GameDirectories localDirectories = new GameDirectories();
localDirectories.setUserFile(false);
try (ProfileEnvironment ignored = new ProfileEnvironment(localDirectories, userDirectories)) {
Profiles.init();
try (GameDirectoryEnvironment ignored = new GameDirectoryEnvironment(localDirectories, userDirectories)) {
GameDirectoryManager.init();
Profile localProfile = assertSingleDefaultProfile(localDirectories, PortablePath.of(".minecraft"));
Profile userProfile = assertSingleDefaultProfile(
GameDirectory localGameDirectory = assertSingleDefaultGameDirectory(localDirectories, PortablePath.of(".minecraft"));
GameDirectory userGameDirectory = assertSingleDefaultGameDirectory(
userDirectories,
PortablePath.fromPath(Metadata.MINECRAFT_DIRECTORY));
assertEquals(2, Profiles.getProfiles().size());
assertTrue(Profiles.getProfiles().contains(localProfile));
assertTrue(Profiles.getProfiles().contains(userProfile));
assertEquals(2, GameDirectoryManager.getGameDirectories().size());
assertTrue(GameDirectoryManager.getGameDirectories().contains(localGameDirectory));
assertTrue(GameDirectoryManager.getGameDirectories().contains(userGameDirectory));
}
}
/// Tests that the selected profile getter restores a valid selection instead of returning null.
/// Tests that the selected game directory getter restores a valid selection instead of returning null.
@Test
public void selectedProfileGetterNeverReturnsNullWhenProfilesAreLoaded() throws ReflectiveOperationException {
public void selectedGameDirectoryGetterNeverReturnsNullWhenGameDirectoriesAreLoaded() throws ReflectiveOperationException {
GameDirectories userDirectories = new GameDirectories();
userDirectories.setUserFile(true);
GameDirectories localDirectories = new GameDirectories();
localDirectories.setUserFile(false);
try (ProfileEnvironment ignored = new ProfileEnvironment(localDirectories, userDirectories)) {
Profiles.init();
try (GameDirectoryEnvironment ignored = new GameDirectoryEnvironment(localDirectories, userDirectories)) {
GameDirectoryManager.init();
Profile selected = Profiles.getSelectedProfile();
GameDirectory selected = GameDirectoryManager.getSelectedGameDirectory();
assertNotNull(selected);
assertSame(selected, Profiles.getSelectedProfile());
assertTrue(Profiles.getProfiles().contains(selected));
assertSame(selected, GameDirectoryManager.getSelectedGameDirectory());
assertTrue(GameDirectoryManager.getGameDirectories().contains(selected));
assertEquals(selected.getId(), settings().selectedGameDirectoryProperty().get());
}
}
/// Temporary static state override for profile tests.
private static final class ProfileEnvironment implements AutoCloseable {
/// Tests that a repository follows changes to its game directory path.
@Test
public void repositoryDirectoryFollowsGameDirectoryPath() throws ReflectiveOperationException {
GameDirectories userDirectories = new GameDirectories();
userDirectories.setUserFile(true);
GameDirectories localDirectories = new GameDirectories();
localDirectories.setUserFile(false);
GameDirectory gameDirectory = new GameDirectory(
GameDirectoryID.generate(),
LocalizedText.plain("Local"),
PortablePath.of("local/Dev"));
localDirectories.getGameDirectories().add(gameDirectory);
try (GameDirectoryEnvironment ignored = new GameDirectoryEnvironment(localDirectories, userDirectories)) {
GameDirectoryManager.init();
HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository();
assertSame(gameDirectory, repository.getGameDirectory());
assertEquals(gameDirectory.getPath().toPath(), repository.getBaseDirectory());
PortablePath newPath = PortablePath.of("local/Renamed");
gameDirectory.setPath(newPath);
assertEquals(newPath.toPath(), repository.getBaseDirectory());
}
}
/// Temporary static state override for game directory tests.
private static final class GameDirectoryEnvironment implements AutoCloseable {
/// The reflected SettingsManager local game directories field.
private final Field localGameDirectoriesField;
@@ -396,14 +424,20 @@ public final class GameDirectoriesTest {
/// The reflected SettingsManager user game directories access field.
private final Field userGameDirectoriesAccessField;
/// The reflected Profiles initialized field.
/// The reflected GameDirectoryManager initialized field.
private final Field initializedField;
/// The reflected Profiles selected profile property.
private final ObjectProperty<Profile> selectedProfile;
/// The reflected GameDirectoryManager selected game directory property.
private final ObjectProperty<GameDirectory> selectedGameDirectory;
/// The merged profile list used by Profiles.
private final ObservableList<Profile> mergedProfiles;
/// The reflected GameDirectoryManager selected repository property.
private final ObjectProperty<HMCLGameRepository> selectedRepository;
/// The merged game directory list used by GameDirectoryManager.
private final ObservableList<GameDirectory> mergedGameDirectories;
/// The repositories mapped by GameDirectoryManager.
private final Map<GameDirectory, HMCLGameRepository> repositories;
/// The previous local game directories instance.
private final Object previousLocalGameDirectories;
@@ -420,34 +454,44 @@ public final class GameDirectoriesTest {
/// The previous user game directories access.
private final SettingFileAccess previousUserGameDirectoriesAccess;
/// The previous Profiles initialization state.
/// The previous GameDirectoryManager initialization state.
private final boolean previousInitialized;
/// The previous selected profile.
private final Profile previousSelectedProfile;
/// The previous selected game directory.
private final GameDirectory previousSelectedGameDirectory;
/// The previous merged profiles.
private final List<Profile> previousMergedProfiles;
/// The previous selected repository.
private final HMCLGameRepository previousSelectedRepository;
/// Replaces profile-related static state with the given stores.
private ProfileEnvironment(GameDirectories localDirectories, GameDirectories userDirectories)
/// The previous merged game directories.
private final List<GameDirectory> previousMergedGameDirectories;
/// The previous repository map entries.
private final Map<GameDirectory, HMCLGameRepository> previousRepositories;
/// Replaces game-directory-related static state with the given stores.
private GameDirectoryEnvironment(GameDirectories localDirectories, GameDirectories userDirectories)
throws ReflectiveOperationException {
localGameDirectoriesField = SettingsManager.class.getDeclaredField("localGameDirectories");
userGameDirectoriesField = SettingsManager.class.getDeclaredField("userGameDirectories");
launcherSettingsField = SettingsManager.class.getDeclaredField("launcherSettings");
localGameDirectoriesAccessField = SettingsManager.class.getDeclaredField("localGameDirectoriesAccess");
userGameDirectoriesAccessField = SettingsManager.class.getDeclaredField("userGameDirectoriesAccess");
initializedField = Profiles.class.getDeclaredField("initialized");
Field selectedProfileField = Profiles.class.getDeclaredField("selectedProfile");
Field mergedProfilesField = Profiles.class.getDeclaredField("mergedProfiles");
initializedField = GameDirectoryManager.class.getDeclaredField("initialized");
Field selectedGameDirectoryField = GameDirectoryManager.class.getDeclaredField("selectedGameDirectory");
Field selectedRepositoryField = GameDirectoryManager.class.getDeclaredField("selectedRepository");
Field mergedGameDirectoriesField = GameDirectoryManager.class.getDeclaredField("mergedGameDirectories");
Field repositoriesField = GameDirectoryManager.class.getDeclaredField("repositories");
localGameDirectoriesField.setAccessible(true);
userGameDirectoriesField.setAccessible(true);
launcherSettingsField.setAccessible(true);
localGameDirectoriesAccessField.setAccessible(true);
userGameDirectoriesAccessField.setAccessible(true);
initializedField.setAccessible(true);
selectedProfileField.setAccessible(true);
mergedProfilesField.setAccessible(true);
selectedGameDirectoryField.setAccessible(true);
selectedRepositoryField.setAccessible(true);
mergedGameDirectoriesField.setAccessible(true);
repositoriesField.setAccessible(true);
previousLocalGameDirectories = localGameDirectoriesField.get(null);
previousUserGameDirectories = userGameDirectoriesField.get(null);
@@ -457,16 +501,28 @@ public final class GameDirectoriesTest {
previousInitialized = initializedField.getBoolean(null);
@SuppressWarnings("unchecked")
ObjectProperty<Profile> selectedProfile =
(ObjectProperty<Profile>) selectedProfileField.get(null);
this.selectedProfile = selectedProfile;
previousSelectedProfile = selectedProfile.get();
ObjectProperty<GameDirectory> selectedGameDirectory =
(ObjectProperty<GameDirectory>) selectedGameDirectoryField.get(null);
this.selectedGameDirectory = selectedGameDirectory;
previousSelectedGameDirectory = selectedGameDirectory.get();
@SuppressWarnings("unchecked")
ObservableList<Profile> mergedProfiles =
(ObservableList<Profile>) mergedProfilesField.get(null);
this.mergedProfiles = mergedProfiles;
previousMergedProfiles = List.copyOf(mergedProfiles);
ObjectProperty<HMCLGameRepository> selectedRepository =
(ObjectProperty<HMCLGameRepository>) selectedRepositoryField.get(null);
this.selectedRepository = selectedRepository;
previousSelectedRepository = selectedRepository.get();
@SuppressWarnings("unchecked")
ObservableList<GameDirectory> mergedGameDirectories =
(ObservableList<GameDirectory>) mergedGameDirectoriesField.get(null);
this.mergedGameDirectories = mergedGameDirectories;
previousMergedGameDirectories = List.copyOf(mergedGameDirectories);
@SuppressWarnings("unchecked")
Map<GameDirectory, HMCLGameRepository> repositories =
(Map<GameDirectory, HMCLGameRepository>) repositoriesField.get(null);
this.repositories = repositories;
previousRepositories = Map.copyOf(repositories);
localGameDirectoriesField.set(null, localDirectories);
userGameDirectoriesField.set(null, userDirectories);
@@ -474,7 +530,9 @@ public final class GameDirectoriesTest {
localGameDirectoriesAccessField.set(null, SettingFileAccess.READ_WRITE);
userGameDirectoriesAccessField.set(null, SettingFileAccess.READ_WRITE);
initializedField.setBoolean(null, false);
mergedProfiles.clear();
mergedGameDirectories.clear();
repositories.clear();
selectedRepository.set(null);
}
/// Sets the local game directories access used by [SettingsManager].
@@ -490,10 +548,13 @@ public final class GameDirectoriesTest {
/// Restores the previous static state.
@Override
public void close() throws ReflectiveOperationException {
if (previousSelectedProfile != null) {
selectedProfile.set(previousSelectedProfile);
if (previousSelectedGameDirectory != null) {
selectedGameDirectory.set(previousSelectedGameDirectory);
}
mergedProfiles.setAll(previousMergedProfiles);
selectedRepository.set(previousSelectedRepository);
mergedGameDirectories.setAll(previousMergedGameDirectories);
repositories.clear();
repositories.putAll(previousRepositories);
localGameDirectoriesField.set(null, previousLocalGameDirectories);
userGameDirectoriesField.set(null, previousUserGameDirectories);
launcherSettingsField.set(null, previousLauncherSettings);
@@ -503,25 +564,25 @@ public final class GameDirectoriesTest {
}
}
/// Returns the only default profile in the given store, asserting its path.
private static Profile assertSingleDefaultProfile(GameDirectories gameDirectories, PortablePath path) {
/// Returns the only default game directory in the given store, asserting its path.
private static GameDirectory assertSingleDefaultGameDirectory(GameDirectories gameDirectories, PortablePath path) {
assertEquals(1, gameDirectories.getGameDirectories().size());
Profile profile = gameDirectories.getGameDirectories().get(0);
assertEquals(path.isAbsolute(), profile.getPath().isAbsolute());
assertEquals(path.getPath(), profile.getPath().getPath());
return profile;
GameDirectory gameDirectory = gameDirectories.getGameDirectories().get(0);
assertEquals(path.isAbsolute(), gameDirectory.getPath().isAbsolute());
assertEquals(path.getPath(), gameDirectory.getPath().getPath());
return gameDirectory;
}
/// Tests that profiles must be deserialized with a non-nil ID.
/// Tests that game directories must be deserialized with a non-nil ID.
@Test
public void rejectsNilProfileId() {
public void rejectsNilGameDirectoryId() {
assertThrows(JsonParseException.class, () -> JsonUtils.GSON.fromJson("""
{
"id": "game-directory:00000000-0000-0000-0000-000000000000",
"name": "Dev",
"path": "versions/Dev"
}
""", Profile.class));
""", GameDirectory.class));
}
/// Tests that newer minor-version schemas are reported as unsupported.
@@ -132,10 +132,10 @@ public final class GameSettingsPresetsTest {
LegacyConfigMigrator.migrateLegacyPresetSettings(gameDirectories, presets, configurations);
assertEquals(1, presets.getPresets().size());
Profile profile = gameDirectories.getGameDirectories().get(0);
GameDirectory gameDirectory = gameDirectories.getGameDirectories().get(0);
GameSettings.Preset preset = presets.getPresets().get(0);
assertEquals(profile.getLegacyGameSettings(), preset.idProperty().getValue());
assertNotEquals(profile.getId(), preset.idProperty().getValue());
assertEquals(gameDirectory.getLegacyGameSettings(), preset.idProperty().getValue());
assertNotEquals(gameDirectory.getId(), preset.idProperty().getValue());
assertEquals(2048, preset.maxMemoryProperty().getValue());
}
}
+2 -2
View File
@@ -18,12 +18,12 @@
"directories": {
"type": "array",
"items": {
"$ref": "#/$defs/profile"
"$ref": "#/$defs/gameDirectory"
}
}
},
"$defs": {
"profile": {
"gameDirectory": {
"type": "object",
"additionalProperties": true,
"required": [