diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java index 75fba035457..56c62ecf214 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameLauncher.java @@ -42,16 +42,16 @@ */ public final class HMCLGameLauncher extends DefaultLauncher { - public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options) { - this(repository, version, authInfo, options, null); + public HMCLGameLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options) { + this(repository, manifest, authInfo, options, null); } - public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) { - this(repository, version, authInfo, options, listener, true); + public HMCLGameLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) { + this(repository, manifest, authInfo, options, listener, true); } - public HMCLGameLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) { - super(repository, version, authInfo, options, listener, daemon); + public HMCLGameLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) { + super(repository, manifest, authInfo, options, listener, daemon); } @Override @@ -66,7 +66,7 @@ private void generateOptionsTxt() { if (options.isDisableAutoGameOptions()) return; - Path runDir = repository.getRunDirectory(version.getId()); + Path runDir = repository.getRunDirectory(manifest.id()); Path optionsFile = runDir.resolve("options.txt"); Path configFolder = runDir.resolve("config"); @@ -91,7 +91,7 @@ private void generateOptionsTxt() { * 1.11 ~ 1.12 : zh_cn works fine, zh_CN will display Chinese but the language setting will incorrectly show English as selected * 1.13+ : zh_cn works fine, zh_CN will automatically switch to English */ - GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(version)); + GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(manifest)); if (gameVersion.compareTo("1.1") < 0) return; @@ -161,7 +161,7 @@ protected void appendJvmArgs(CommandBuilder result) { if (options.isAllowAutoAgent() && !options.isNoGeneratedJVMArgs() && !options.isNoGeneratedOptimizingJVMArgs() - && NativePatcher.needPatchMemoryUtil(version, options.getJava().getParsedVersion())) { + && NativePatcher.needPatchMemoryUtil(manifest, options.getJava().getParsedVersion())) { LOG.info("Attempting to patch game with lwjgl-unsafe-agent"); try { result.add("-javaagent:" + extractLwjglUnsafeAgent()); @@ -180,7 +180,7 @@ private Path extractLwjglUnsafeAgent() throws IOException { Library library = new Library(new Artifact("org.glavo", "lwjgl-unsafe-agent", agentVersion)); String fileName = library.getArtifact().getFileName(); - Path agentPath = repository.getLibraryFile(version, library).toAbsolutePath().normalize(); + Path agentPath = repository.getLibraryFile(manifest, library).toAbsolutePath().normalize(); if (agentPath.toString().contains("=")) { throw new IOException("Invalid library path: " + agentPath); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java index b250406fa2d..020da9f7fae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLGameRepository.java @@ -20,8 +20,9 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.reflect.TypeToken; +import javafx.beans.binding.Binding; import javafx.beans.binding.Bindings; -import javafx.beans.binding.StringBinding; +import javafx.beans.binding.ObjectBinding; import javafx.scene.image.Image; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.download.DefaultDependencyManager; @@ -45,7 +46,7 @@ import org.jackhuang.hmcl.setting.ProxyType; import org.jackhuang.hmcl.setting.SettingFileUtils; import org.jackhuang.hmcl.setting.GameSettingsPresetID; -import org.jackhuang.hmcl.setting.VersionIconType; +import org.jackhuang.hmcl.setting.GameInstanceIconType; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.util.Lang; @@ -81,10 +82,10 @@ public final class HMCLGameRepository extends DefaultGameRepository { /// @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) { + public record InstanceReference(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) { } - /// Directory under the version root that stores HMCL-managed instance metadata. + /// Directory under the instance root that stores HMCL-managed instance metadata. private static final String INSTANCE_METADATA_DIRECTORY = ".hmcl"; /// Directory under the instance metadata directory that stores instance configuration files. @@ -100,22 +101,22 @@ public record InstanceReference(HMCLGameRepository repository, @Nullable String private final GameDirectory gameDirectory; /// The selected instance ID persisted for this repository's game directory. - private final StringBinding selectedInstance; + private final ObjectBinding<@Nullable GameInstanceID> selectedInstance; // instance game settings - private final Map instanceGameSettings = new HashMap<>(); + private final Map instanceGameSettings = new HashMap<>(); /// Instance IDs whose local game settings file has already been checked. - private final Set loadedInstanceGameSettings = new HashSet<>(); - private final Set readOnlyInstanceGameSettings = new HashSet<>(); - private final Set beingModpackVersions = new HashSet<>(); + private final Set loadedInstanceGameSettings = new HashSet<>(); + private final Set readOnlyInstanceGameSettings = new HashSet<>(); + private final Set beingModpackInstances = new HashSet<>(); - public final EventManager onVersionIconChanged = new EventManager<>(); + public final EventManager onInstanceIconChanged = new EventManager<>(); /// 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()); + this.selectedInstance = Bindings.valueAt(settings().getSelectedInstance(), gameDirectory.getId()); gameDirectory.pathProperty().addListener((a, b, newValue) -> changeDirectory(newValue.toPath())); } @@ -125,26 +126,26 @@ public GameDirectory getGameDirectory() { } /// Returns the selected instance ID property for this repository's game directory. - public StringBinding selectedInstanceProperty() { + public Binding<@Nullable GameInstanceID> selectedInstanceProperty() { return selectedInstance; } /// Returns the selected instance ID for this repository's game directory. - public @Nullable String getSelectedInstance() { + public @Nullable GameInstanceID 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); + public void setSelectedInstance(@Nullable GameInstanceID instanceId) { + settings().setSelectedInstance(gameDirectory.getId(), instanceId); } - /// Refreshes the selected instance ID after versions are loaded. + /// Refreshes the selected instance ID after instances 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(); + @Nullable GameInstanceID selectedInstance = settings().getSelectedInstance(gameDirectory.getId()); + @Nullable GameInstanceID refreshedInstance = selectedInstance; + if (refreshedInstance == null || !hasInstance(refreshedInstance)) { + refreshedInstance = getInstanceManifests().isEmpty() ? null : getInstanceManifests().iterator().next().id(); } if (!Objects.equals(selectedInstance, refreshedInstance)) { setSelectedInstance(refreshedInstance); @@ -162,24 +163,24 @@ public DefaultDependencyManager getDependency(DownloadProvider downloadProvider) } @Override - public Path getRunDirectory(String id) { - if (beingModpackVersions.contains(id) || isModpack(id)) { - return getVersionRoot(id); + public Path getRunDirectory(GameInstanceID instanceId) { + if (beingModpackInstances.contains(instanceId) || isModpack(instanceId)) { + return getInstanceRoot(instanceId); } - GameSettings.Instance localSetting = getInstanceGameSettings(id); + GameSettings.Instance localSetting = getInstanceGameSettings(instanceId); boolean useInstanceRunningDirectory = localSetting != null && localSetting.getOverrideProperties().contains(GameSettings.PROPERTY_RUNNING_DIRECTORY); String runningDirectory = getSelectedRunningDirectory(localSetting, useInstanceRunningDirectory); if (StringUtils.isBlank(runningDirectory)) { - return useInstanceRunningDirectory ? getVersionRoot(id) : super.getRunDirectory(id); + return useInstanceRunningDirectory ? getInstanceRoot(instanceId) : super.getRunDirectory(instanceId); } try { return Path.of(runningDirectory); } catch (InvalidPathException ignored) { - return getVersionRoot(id); + return getInstanceRoot(instanceId); } } @@ -201,24 +202,24 @@ private String getSelectedRunningDirectory( return Objects.requireNonNullElse(parent.runningDirectoryProperty().getValue(), ""); } - public Stream getDisplayVersions() { - return getVersions().stream() + public Stream getDisplayInstanceManifests() { + return getInstanceManifests().stream() .filter(v -> !v.isHidden()) - .sorted(Comparator.comparing((Version v) -> Lang.requireNonNullElse(v.getReleaseTime(), Instant.EPOCH)) - .thenComparing(v -> VersionNumber.asVersion(v.getId()))); + .sorted(Comparator.comparing((GameInstanceManifest v) -> Lang.requireNonNullElse(v.releaseTime(), Instant.EPOCH)) + .thenComparing(v -> VersionNumber.asVersion(v.id().id()))); } @Override - protected void refreshVersionsImpl() { + protected void refreshImpl() { instanceGameSettings.clear(); loadedInstanceGameSettings.clear(); readOnlyInstanceGameSettings.clear(); - super.refreshVersionsImpl(); - versions.keySet().forEach(this::loadInstanceGameSettings); + super.refreshImpl(); + getInstanceManifests().stream().map(GameInstanceManifest::id).forEach(this::loadInstanceGameSettings); try { Path file = getBaseDirectory().resolve("launcher_profiles.json"); - if (!Files.exists(file) && !versions.isEmpty()) { + if (!Files.exists(file) && !getInstanceManifests().isEmpty()) { Files.createDirectories(file.getParent()); Files.writeString(file, PROFILE); } @@ -229,7 +230,7 @@ protected void refreshVersionsImpl() { public void changeDirectory(Path newDirectory) { setBaseDirectory(newDirectory); - refreshVersionsAsync().start(); + refreshAsync().start(); } private void clean(Path directory) throws IOException { @@ -237,43 +238,43 @@ private void clean(Path directory) throws IOException { FileUtils.deleteDirectory(directory.resolve("logs")); } - public void clean(String id) throws IOException { + public void clean(GameInstanceID instanceId) throws IOException { clean(getBaseDirectory()); - clean(getRunDirectory(id)); + clean(getRunDirectory(instanceId)); } - public void duplicateVersion(String srcId, String dstId, boolean copySaves) throws IOException { - Path srcDir = getVersionRoot(srcId); - Path dstDir = getVersionRoot(dstId); + public void duplicateInstance(GameInstanceID srcId, GameInstanceID dstId, boolean copySaves) throws IOException { + Path srcDir = getInstanceRoot(srcId); + Path dstDir = getInstanceRoot(dstId); - Version fromVersion = getVersion(srcId); + GameInstanceManifest fromManifest = getInstanceManifest(srcId); List blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST); - blackList.add(srcId + ".jar"); - blackList.add(srcId + ".json"); + blackList.add(srcId.id() + ".jar"); + blackList.add(srcId.id() + ".json"); if (!copySaves) blackList.add("saves"); - if (Files.exists(dstDir)) throw new IOException("Version exists"); + if (Files.exists(dstDir)) throw new IOException("Instance exists"); Files.createDirectories(dstDir); FileUtils.copyDirectory(srcDir, dstDir, path -> Modpack.acceptFile(path, blackList, null)); - Path fromJson = srcDir.resolve(srcId + ".json"); - Path fromJar = srcDir.resolve(srcId + ".jar"); - Path toJson = dstDir.resolve(dstId + ".json"); - Path toJar = dstDir.resolve(dstId + ".jar"); + Path fromJson = srcDir.resolve(srcId.id() + ".json"); + Path fromJar = srcDir.resolve(srcId.id() + ".jar"); + Path toJson = dstDir.resolve(dstId.id() + ".json"); + Path toJar = dstDir.resolve(dstId.id() + ".jar"); if (Files.exists(fromJar)) { Files.copy(fromJar, toJar); } Files.copy(fromJson, toJson); - JsonUtils.writeToJsonFile(toJson, fromVersion.setId(dstId).setJar(dstId)); + JsonUtils.writeToJsonFile(toJson, fromManifest.withId(dstId).withJar(dstId)); boolean copyOriginalGameDir; try { - copyOriginalGameDir = !Files.isSameFile(getRunDirectory(srcId), getVersionRoot(srcId)); + copyOriginalGameDir = !Files.isSameFile(getRunDirectory(srcId), getInstanceRoot(srcId)); } catch (IOException e) { copyOriginalGameDir = true; } @@ -292,48 +293,48 @@ public void duplicateVersion(String srcId, String dstId, boolean copySaves) thro FileUtils.copyDirectory(srcGameDir, dstGameDir, path -> Modpack.acceptFile(path, blackList, null)); } - private GameSettings.Instance copyInstanceGameSettings(String id) { - GameSettings.Instance setting = getInstanceGameSettings(id); + private GameSettings.Instance copyInstanceGameSettings(GameInstanceID instanceId) { + GameSettings.Instance setting = getInstanceGameSettings(instanceId); if (setting != null) { return JsonUtils.clone(LauncherSettings.SETTINGS_GSON, setting, TypeToken.get(GameSettings.Instance.class)); } GameSettings.Instance copied = new GameSettings.Instance(); - copied.parentProperty().setValue(getEffectiveGameSettings(id).getPreset().idProperty().getValue()); + copied.parentProperty().setValue(getEffectiveGameSettings(instanceId).getPreset().idProperty().getValue()); return copied; } - /// Returns the HMCL-managed metadata directory under the version root. + /// Returns the HMCL-managed metadata directory under the instance root. /// /// This directory stores instance-scoped files owned by HMCL. - public Path getInstanceMetadataDirectory(String id) { - return getVersionRoot(id).resolve(INSTANCE_METADATA_DIRECTORY); + public Path getInstanceMetadataDirectory(GameInstanceID instanceId) { + return getInstanceRoot(instanceId).resolve(INSTANCE_METADATA_DIRECTORY); } /// Returns the HMCL-managed configuration directory under the instance metadata directory. - public Path getInstanceConfigDirectory(String id) { - return getInstanceMetadataDirectory(id).resolve(INSTANCE_CONFIG_DIRECTORY); + public Path getInstanceConfigDirectory(GameInstanceID instanceId) { + return getInstanceMetadataDirectory(instanceId).resolve(INSTANCE_CONFIG_DIRECTORY); } /// Returns the HMCL-managed state directory under the instance metadata directory. - public Path getInstanceStateDirectory(String id) { - return getInstanceMetadataDirectory(id).resolve(INSTANCE_STATE_DIRECTORY); + public Path getInstanceStateDirectory(GameInstanceID instanceId) { + return getInstanceMetadataDirectory(instanceId).resolve(INSTANCE_STATE_DIRECTORY); } /// Returns the current local game settings path under the instance configuration directory. - private Path getInstanceGameSettingsFile(String id) { - return getInstanceConfigDirectory(id).resolve(INSTANCE_GAME_SETTINGS_FILENAME); + private Path getInstanceGameSettingsFile(GameInstanceID instanceId) { + return getInstanceConfigDirectory(instanceId).resolve(INSTANCE_GAME_SETTINGS_FILENAME); } - private void loadInstanceGameSettings(String id) { - loadedInstanceGameSettings.add(id); - InstanceGameSettingsLoadResult result = loadGameSettingsFile(getInstanceGameSettingsFile(id)); + private void loadInstanceGameSettings(GameInstanceID instanceId) { + loadedInstanceGameSettings.add(instanceId); + InstanceGameSettingsLoadResult result = loadGameSettingsFile(getInstanceGameSettingsFile(instanceId)); if (result.setting() != null) { - initInstanceGameSettings(id, result.setting(), result.allowSave()); + initInstanceGameSettings(instanceId, result.setting(), result.allowSave()); return; } if (!result.allowSave()) { - readOnlyInstanceGameSettings.add(id); + readOnlyInstanceGameSettings.add(instanceId); return; } @@ -344,15 +345,15 @@ private void loadInstanceGameSettings(String id) { LegacyGameSettingsMigrator.InstanceMigrationResult migrationResult = LegacyGameSettingsMigrator.migrateInstanceGameSettings( - this, id, + this, instanceId, legacyParent); if (migrationResult != null) { - initInstanceGameSettings(id, migrationResult.setting()); + initInstanceGameSettings(instanceId, migrationResult.setting()); try { - saveGameSettingsSync(id); + saveGameSettingsSync(instanceId); migrationResult.saveReceipt(); } catch (IOException e) { - LOG.warning("Failed to save migrated instance game settings for " + id, e); + LOG.warning("Failed to save migrated instance game settings for " + instanceId, e); } return; } @@ -416,35 +417,35 @@ private InstanceGameSettingsLoadResult loadGameSettingsFile(Path file) { } } - public @Nullable GameSettings.Instance createInstanceGameSettings(String id) { - if (!hasVersion(id)) { + public @Nullable GameSettings.Instance createInstanceGameSettings(GameInstanceID instanceId) { + if (!hasInstance(instanceId)) { return null; } - if (readOnlyInstanceGameSettings.contains(id)) { + if (readOnlyInstanceGameSettings.contains(instanceId)) { return null; } - if (instanceGameSettings.containsKey(id)) { - return getInstanceGameSettings(id); + if (instanceGameSettings.containsKey(instanceId)) { + return getInstanceGameSettings(instanceId); } GameSettings.Instance setting = new GameSettings.Instance(); - return initInstanceGameSettings(id, setting); + return initInstanceGameSettings(instanceId, setting); } - private GameSettings.Instance initInstanceGameSettings(String id, GameSettings.Instance setting) { - return initInstanceGameSettings(id, setting, true); + private GameSettings.Instance initInstanceGameSettings(GameInstanceID instanceId, GameSettings.Instance setting) { + return initInstanceGameSettings(instanceId, setting, true); } - private GameSettings.Instance initInstanceGameSettings(String id, GameSettings.Instance setting, boolean allowSave) { + private GameSettings.Instance initInstanceGameSettings(GameInstanceID instanceId, GameSettings.Instance setting, boolean allowSave) { normalizeRunningDirectoryOverride(setting); setting.setSavable(allowSave); - loadedInstanceGameSettings.add(id); - instanceGameSettings.put(id, setting); + loadedInstanceGameSettings.add(instanceId); + instanceGameSettings.put(instanceId, setting); if (allowSave) { - readOnlyInstanceGameSettings.remove(id); - setting.addListener(a -> saveGameSettings(id)); + readOnlyInstanceGameSettings.remove(instanceId); + setting.addListener(a -> saveGameSettings(instanceId)); } else { - readOnlyInstanceGameSettings.add(id); + readOnlyInstanceGameSettings.add(instanceId); } return setting; } @@ -457,59 +458,59 @@ private void normalizeRunningDirectoryOverride(GameSettings.Instance setting) { } @Nullable - public GameSettings.Instance getInstanceGameSettings(String id) { - if (!loadedInstanceGameSettings.contains(id)) { - loadInstanceGameSettings(id); + public GameSettings.Instance getInstanceGameSettings(GameInstanceID instanceId) { + if (!loadedInstanceGameSettings.contains(instanceId)) { + loadInstanceGameSettings(instanceId); } - return instanceGameSettings.get(id); + return instanceGameSettings.get(instanceId); } @Nullable - public GameSettings.Instance getInstanceGameSettingsOrCreate(String id) { - GameSettings.Instance setting = getInstanceGameSettings(id); + public GameSettings.Instance getInstanceGameSettingsOrCreate(GameInstanceID instanceId) { + GameSettings.Instance setting = getInstanceGameSettings(instanceId); if (setting == null) { - setting = createInstanceGameSettings(id); + setting = createInstanceGameSettings(instanceId); } return setting; } /// Returns whether the instance-specific game settings file cannot be overwritten safely. /// - /// @param id the instance ID + /// @param instanceId the instance ID /// @return whether the instance settings are loaded in read-only mode - public boolean isInstanceGameSettingsReadOnly(String id) { - if (!loadedInstanceGameSettings.contains(id)) { - loadInstanceGameSettings(id); + public boolean isInstanceGameSettingsReadOnly(GameInstanceID instanceId) { + if (!loadedInstanceGameSettings.contains(instanceId)) { + loadInstanceGameSettings(instanceId); } - return readOnlyInstanceGameSettings.contains(id); + return readOnlyInstanceGameSettings.contains(instanceId); } /// Backs up and overwrites the instance-specific game settings file with the currently loaded settings. /// - /// @param id the instance ID - public void forceOverwriteInstanceGameSettings(String id) { - if (!loadedInstanceGameSettings.contains(id)) { - loadInstanceGameSettings(id); + /// @param instanceId the instance ID + public void forceOverwriteInstanceGameSettings(GameInstanceID instanceId) { + if (!loadedInstanceGameSettings.contains(instanceId)) { + loadInstanceGameSettings(instanceId); } - GameSettings.Instance setting = instanceGameSettings.get(id); + GameSettings.Instance setting = instanceGameSettings.get(instanceId); if (setting == null) { setting = new GameSettings.Instance(); - instanceGameSettings.put(id, setting); - loadedInstanceGameSettings.add(id); + instanceGameSettings.put(instanceId, setting); + loadedInstanceGameSettings.add(instanceId); } boolean installAutoSave = !setting.isSavable(); - Path file = getInstanceGameSettingsFile(id).toAbsolutePath().normalize(); + Path file = getInstanceGameSettingsFile(instanceId).toAbsolutePath().normalize(); SettingFileUtils.backupInvalidConfig(file); setting.setSchema(GameSettings.Instance.CURRENT_SCHEMA); setting.setSavable(true); setting.setBackupOnNextSave(false); - readOnlyInstanceGameSettings.remove(id); - saveGameSettings(id); + readOnlyInstanceGameSettings.remove(instanceId); + saveGameSettings(instanceId); if (installAutoSave) { - setting.addListener(a -> saveGameSettings(id)); + setting.addListener(a -> saveGameSettings(instanceId)); } } @@ -520,35 +521,35 @@ public GameSettings.Preset getParentGameSettings(@Nullable GameSettings.Instance return parentSetting != null ? parentSetting : SettingsManager.getDefaultGameSettingsPresetOrCreate(); } - public GameSettings.Effective getEffectiveGameSettings(String id) { - GameSettings.Instance instance = getInstanceGameSettings(id); + public GameSettings.Effective getEffectiveGameSettings(GameInstanceID instanceId) { + GameSettings.Instance instance = getInstanceGameSettings(instanceId); return GameSettings.resolve(getParentGameSettings(instance), instance); } - public void applyDefaultIsolationSetting(String id) { - if (!hasVersion(id)) { + public void applyDefaultIsolationSetting(GameInstanceID instanceId) { + if (!hasInstance(instanceId)) { return; } - GameSettings.Instance instanceSetting = getInstanceGameSettings(id); + GameSettings.Instance instanceSetting = getInstanceGameSettings(instanceId); GameSettings.Preset preset = getParentGameSettings(instanceSetting); DefaultIsolationType type = Lang.requireNonNullElse(preset.defaultIsolationTypeProperty().getValue(), DefaultIsolationType.MODDED); boolean isolated = switch (type) { case NEVER -> false; case ALWAYS -> true; - case MODDED -> LibraryAnalyzer.isModded(this, getVersion(id).resolve(this)); + case MODDED -> LibraryAnalyzer.isModded(getResolvedInstanceManifest(instanceId)); }; if (isolated) { - GameSettings.Instance setting = instanceSetting != null ? instanceSetting : getInstanceGameSettingsOrCreate(id); + GameSettings.Instance setting = instanceSetting != null ? instanceSetting : getInstanceGameSettingsOrCreate(instanceId); if (setting != null) { setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY); } } } - public Optional getVersionIconFile(String id) { - Path root = getVersionRoot(id); + public Optional getInstanceIconFile(GameInstanceID instanceId) { + Path root = getInstanceRoot(instanceId); for (String extension : FXUtils.IMAGE_EXTENSIONS) { Path file = root.resolve("icon." + extension); @@ -560,19 +561,19 @@ public Optional getVersionIconFile(String id) { return Optional.empty(); } - public void setVersionIconFile(String id, Path iconFile) throws IOException { + public void setInstanceIconFile(GameInstanceID instanceId, Path iconFile) throws IOException { String ext = FileUtils.getExtension(iconFile).toLowerCase(Locale.ROOT); if (!FXUtils.IMAGE_EXTENSIONS.contains(ext)) { throw new IllegalArgumentException("Unsupported icon file: " + ext); } - deleteIconFile(id); + deleteIconFile(instanceId); - FileUtils.copyFile(iconFile, getVersionRoot(id).resolve("icon." + ext)); + FileUtils.copyFile(iconFile, getInstanceRoot(instanceId).resolve("icon." + ext)); } - public void deleteIconFile(String id) { - Path root = getVersionRoot(id); + public void deleteIconFile(GameInstanceID instanceId) { + Path root = getInstanceRoot(instanceId); for (String extension : FXUtils.IMAGE_EXTENSIONS) { Path file = root.resolve("icon." + extension); try { @@ -583,69 +584,69 @@ public void deleteIconFile(String id) { } } - public Image getVersionIconImage(@Nullable String id) { - if (id == null || !isLoaded()) - return VersionIconType.DEFAULT.getIcon(); + public Image getInstanceIconImage(@Nullable GameInstanceID instanceId) { + if (instanceId == null || !isLoaded()) + return GameInstanceIconType.DEFAULT.getIcon(); - GameSettings.Instance setting = getInstanceGameSettings(id); - VersionIconType iconType = setting != null ? Lang.requireNonNullElse(setting.iconProperty().getValue(), VersionIconType.DEFAULT) : VersionIconType.DEFAULT; + GameSettings.Instance setting = getInstanceGameSettings(instanceId); + GameInstanceIconType iconType = setting != null ? Lang.requireNonNullElse(setting.iconProperty().getValue(), GameInstanceIconType.DEFAULT) : GameInstanceIconType.DEFAULT; - if (iconType == VersionIconType.DEFAULT) { - Version version = getVersion(id).resolve(this); - Optional iconFile = getVersionIconFile(id); + if (iconType == GameInstanceIconType.DEFAULT) { + GameInstanceManifest.Resolved resolvedInstanceManifest = getResolvedInstanceManifest(instanceId); + Optional iconFile = getInstanceIconFile(instanceId); if (iconFile.isPresent()) { try { return FXUtils.loadImage(iconFile.get(), 64, 64, true, true); } catch (Exception e) { - LOG.warning("Failed to load version icon of " + id, e); + LOG.warning("Failed to load instance icon of " + instanceId, e); } } - if (LibraryAnalyzer.isModded(this, version)) { - LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null); + if (LibraryAnalyzer.isModded(resolvedInstanceManifest)) { + LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(resolvedInstanceManifest, null); if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FABRIC)) - return VersionIconType.FABRIC.getIcon(); + return GameInstanceIconType.FABRIC.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.QUILT)) - return VersionIconType.QUILT.getIcon(); + return GameInstanceIconType.QUILT.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LEGACY_FABRIC)) - return VersionIconType.LEGACY_FABRIC.getIcon(); + return GameInstanceIconType.LEGACY_FABRIC.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.NEO_FORGE)) - return VersionIconType.NEO_FORGE.getIcon(); + return GameInstanceIconType.NEO_FORGE.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.FORGE)) - return VersionIconType.FORGE.getIcon(); + return GameInstanceIconType.FORGE.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) - return VersionIconType.CLEANROOM.getIcon(); + return GameInstanceIconType.CLEANROOM.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.LITELOADER)) - return VersionIconType.CHICKEN.getIcon(); + return GameInstanceIconType.CHICKEN.getIcon(); else if (libraryAnalyzer.has(LibraryAnalyzer.LibraryType.OPTIFINE)) - return VersionIconType.OPTIFINE.getIcon(); + return GameInstanceIconType.OPTIFINE.getIcon(); } - String gameVersion = getGameVersion(version).orElse(null); + String gameVersion = getGameVersion(resolvedInstanceManifest.launchManifest()).orElse(null); if (gameVersion != null) { GameVersionNumber versionNumber = GameVersionNumber.asGameVersion(gameVersion); if (versionNumber.isAprilFools()) { - return VersionIconType.APRIL_FOOLS.getIcon(); + return GameInstanceIconType.APRIL_FOOLS.getIcon(); } else if (versionNumber instanceof GameVersionNumber.LegacySnapshot) { - return VersionIconType.COMMAND.getIcon(); + return GameInstanceIconType.COMMAND.getIcon(); } else if (versionNumber instanceof GameVersionNumber.Old) { - return VersionIconType.CRAFT_TABLE.getIcon(); + return GameInstanceIconType.CRAFT_TABLE.getIcon(); } } - return VersionIconType.GRASS.getIcon(); + return GameInstanceIconType.GRASS.getIcon(); } else { return iconType.getIcon(); } } - public void saveGameSettings(String id) { - if (!instanceGameSettings.containsKey(id) || readOnlyInstanceGameSettings.contains(id)) + public void saveGameSettings(GameInstanceID instanceId) { + if (!instanceGameSettings.containsKey(instanceId) || readOnlyInstanceGameSettings.contains(instanceId)) return; - GameSettings.Instance setting = instanceGameSettings.get(id); + GameSettings.Instance setting = instanceGameSettings.get(instanceId); if (setting == null) { return; } - Path file = getInstanceGameSettingsFile(id).toAbsolutePath().normalize(); + Path file = getInstanceGameSettingsFile(instanceId).toAbsolutePath().normalize(); try { Files.createDirectories(file.getParent()); } catch (IOException e) { @@ -661,19 +662,19 @@ public void saveGameSettings(String id) { /// Saves instance-specific game settings synchronously. /// - /// @param id the instance ID + /// @param instanceId the instance ID /// @throws IOException if saving the file fails - private void saveGameSettingsSync(String id) throws IOException { - if (!instanceGameSettings.containsKey(id) || readOnlyInstanceGameSettings.contains(id)) { + private void saveGameSettingsSync(GameInstanceID instanceId) throws IOException { + if (!instanceGameSettings.containsKey(instanceId) || readOnlyInstanceGameSettings.contains(instanceId)) { return; } - GameSettings.Instance setting = instanceGameSettings.get(id); + GameSettings.Instance setting = instanceGameSettings.get(instanceId); if (setting == null) { return; } - Path file = getInstanceGameSettingsFile(id).toAbsolutePath().normalize(); + Path file = getInstanceGameSettingsFile(instanceId).toAbsolutePath().normalize(); Files.createDirectories(file.getParent()); if (setting.isBackupOnNextSave()) { setting.setBackupOnNextSave(false); @@ -691,17 +692,17 @@ private record InstanceGameSettingsLoadResult( boolean allowSave) { } - public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVersion, Path gameDir, List javaAgents, List javaArguments, boolean makeLaunchScript) { - GameSettings.Effective vs = getEffectiveGameSettings(version); + public LaunchOptions.Builder getLaunchOptions(GameInstanceID instanceId, JavaRuntime javaVersion, Path gameDir, List javaAgents, List javaArguments, boolean makeLaunchScript) { + GameSettings.Effective vs = getEffectiveGameSettings(instanceId); boolean noJVMOptions = vs.getInheritable(GameSettings::noJVMOptionsProperty); boolean autoMemory = vs.get(GameSettings::autoMemoryProperty); - GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(getGameVersion(version)); + GameVersionNumber gameVersionNumber = GameVersionNumber.asGameVersion(getGameVersion(instanceId)); LaunchOptions.Builder builder = new LaunchOptions.Builder() .setGameDir(gameDir) .setJava(javaVersion) .setVersionType(Metadata.TITLE) - .setVersionName(version) + .setVersionName(instanceId.id()) .setProfileName(Metadata.TITLE) .setGameArguments(StringUtils.tokenize(vs.get(GameSettings::gameArgumentsProperty))) .setOverrideJavaArguments(StringUtils.tokenize(vs.get(GameSettings::jvmOptionsProperty))) @@ -750,7 +751,7 @@ public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVe builder.setQuickPlayOption(quickPlayOption); } - Path json = getModpackConfiguration(version); + Path json = getModpackConfiguration(instanceId); if (Files.exists(json)) { try { String jsonText = Files.readString(json); @@ -769,27 +770,27 @@ public LaunchOptions.Builder getLaunchOptions(String version, JavaRuntime javaVe } @Override - public Path getModpackConfiguration(String version) { - return getVersionRoot(version).resolve("modpack.cfg"); + public Path getModpackConfiguration(GameInstanceID instanceId) { + return getInstanceRoot(instanceId).resolve("modpack.cfg"); } - public void markVersionAsModpack(String id) { - beingModpackVersions.add(id); + public void markInstanceAsModpack(GameInstanceID instanceId) { + beingModpackInstances.add(instanceId); } - public void undoMark(String id) { - beingModpackVersions.remove(id); + public void undoMark(GameInstanceID instanceId) { + beingModpackInstances.remove(instanceId); } - public void markVersionLaunchedAbnormally(String id) { + public void markInstanceLaunchedAbnormally(GameInstanceID instanceId) { try { - Files.createFile(getVersionRoot(id).resolve(".abnormal")); + Files.createFile(getInstanceRoot(instanceId).resolve(".abnormal")); } catch (IOException ignored) { } } - public boolean unmarkVersionLaunchedAbnormally(String id) { - Path file = getVersionRoot(id).resolve(".abnormal"); + public boolean unmarkInstanceLaunchedAbnormally(GameInstanceID instanceId) { + Path file = getInstanceRoot(instanceId).resolve(".abnormal"); if (Files.isRegularFile(file)) { try { Files.delete(file); @@ -806,35 +807,42 @@ public boolean unmarkVersionLaunchedAbnormally(String id) { private static final String PROFILE = "{\"selectedProfile\": \"(Default)\",\"profiles\": {\"(Default)\": {\"name\": \"(Default)\"}},\"clientToken\": \"88888888-8888-8888-8888-888888888888\"}"; - // These version ids are forbidden because they may conflict with modpack configuration filenames - private static final Set FORBIDDEN_VERSION_IDS = new HashSet<>(Arrays.asList( - "modpack", "minecraftinstance", "manifest")); + // These instance ids are forbidden because they may conflict with modpack configuration filenames + private static final Set FORBIDDEN_INSTANCE_IDS = Set.of("modpack", "minecraftinstance", "manifest"); - public static boolean isValidVersionId(String id) { - if (FORBIDDEN_VERSION_IDS.contains(id)) + public static boolean isValidInstanceId(String id) { + if (FORBIDDEN_INSTANCE_IDS.contains(id)) return false; if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS && - FORBIDDEN_VERSION_IDS.contains(id.toLowerCase(Locale.ROOT))) + FORBIDDEN_INSTANCE_IDS.contains(id.toLowerCase(Locale.ROOT))) return false; return FileUtils.isNameValidForJar(id); } /** - * Returns true if the given version id conflicts with an existing version. + * Returns true if the given instance id conflicts with an existing instance. */ - public boolean versionIdConflicts(String id) { + public boolean instanceIdConflicts(String instanceId) { + try { + return instanceIdConflicts(new GameInstanceID(instanceId)); + } catch (IllegalArgumentException e) { + return false; + } + } + + public boolean instanceIdConflicts(GameInstanceID id) { if (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS) { // on Windows, filenames are case-insensitive - for (String existingId : versions.keySet()) { - if (existingId.equalsIgnoreCase(id)) { + for (GameInstanceManifest manifest : getInstanceManifests()) { + if (manifest.id().toString().equalsIgnoreCase(id.toString())) { return true; } } return false; } else { - return versions.containsKey(id); + return hasInstance(id); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java index e7de3f914c0..e55d085f31b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackInstallTask.java @@ -37,29 +37,29 @@ public final class HMCLModpackInstallTask extends Task { private final Path zipFile; - private final String name; + private final GameInstanceID instanceId; private final HMCLGameRepository repository; private final DefaultDependencyManager dependency; private final Modpack modpack; private final List> dependencies = new ArrayList<>(1); private final List> dependents = new ArrayList<>(4); - public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpack modpack, String name) { + public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpack modpack, GameInstanceID instanceId) { this.repository = repository; this.dependency = repository.getDependency(); this.zipFile = zipFile; - this.name = name; + this.instanceId = instanceId; this.modpack = modpack; - Path run = repository.getRunDirectory(name); - Path json = repository.getModpackConfiguration(name); - if (repository.hasVersion(name) && Files.notExists(json)) - throw new IllegalArgumentException("Version " + name + " already exists"); + Path run = repository.getRunDirectory(this.instanceId); + Path json = repository.getModpackConfiguration(this.instanceId); + if (repository.hasInstance(this.instanceId) && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " already exists"); - dependents.add(dependency.gameBuilder().name(name).gameVersion(modpack.getGameVersion()).buildAsync()); + dependents.add(dependency.newGameBuilder().name(this.instanceId).gameVersion(modpack.getGameVersion()).buildAsync()); onDone().register(event -> { - if (event.isFailed()) repository.removeVersionFromDisk(name); + if (event.isFailed()) repository.removeInstanceFromDisk(this.instanceId); }); ModpackConfiguration config = null; @@ -68,12 +68,12 @@ public HMCLModpackInstallTask(HMCLGameRepository repository, Path zipFile, Modpa config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(Modpack.class)); if (!HMCLModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Version " + name + " is not a HMCL modpack. Cannot update this version."); + throw new IllegalArgumentException("Version " + instanceId + " is not a HMCL modpack. Cannot update this version."); } } catch (JsonParseException | IOException ignore) { } dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/minecraft"), it -> !"pack.json".equals(it), config)); - dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack")); + dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/minecraft"), modpack, HMCLModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(this.instanceId)).withStage("hmcl.modpack")); } @Override @@ -89,9 +89,9 @@ public List> getDependents() { @Override public void execute() throws Exception { String json = CompressingUtils.readTextZipEntry(zipFile, "minecraft/pack.json"); - Version originalVersion = JsonUtils.GSON.fromJson(json, Version.class).setId(name).setJar(null); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalVersion, null); - Task libraryTask = Task.supplyAsync(() -> originalVersion); + GameInstanceManifest originalManifest = JsonUtils.GSON.fromJson(json, GameInstanceManifest.class).withId(instanceId).withJar(null); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(originalManifest, null); + Task libraryTask = Task.supplyAsync(() -> originalManifest); // reinstall libraries // libraries of Forge and OptiFine should be obtained by installation. for (LibraryAnalyzer.LibraryMark mark : analyzer) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java index 257f39ce78e..b9db4d364ce 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/HMCLModpackProvider.java @@ -42,12 +42,12 @@ public String getName() { } @Override - public Task createCompletionTask(DefaultDependencyManager dependencyManager, String version) { + public Task createCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { return null; } @Override - public Task createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { + public Task createUpdateTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { if (!(modpack.getManifest() instanceof HMCLModpackManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); @@ -55,7 +55,7 @@ public Task createUpdateTask(DefaultDependencyManager dependencyManager, Stri throw new IllegalArgumentException("HMCLModpackProvider requires HMCLGameRepository"); } - return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new HMCLModpackInstallTask(repository, zipFile, modpack, name)); + return new ModpackUpdateTask(dependencyManager.getGameRepository(), instanceId, new HMCLModpackInstallTask(repository, zipFile, modpack, instanceId)); } @Override @@ -63,21 +63,21 @@ public Modpack readManifest(ZipArchiveReader file, Path path, Charset encoding) String manifestJson = CompressingUtils.readTextZipEntry(file, "modpack.json"); Modpack manifest = JsonUtils.fromNonNullJson(manifestJson, HMCLModpack.class).setEncoding(encoding); String gameJson = CompressingUtils.readTextZipEntry(file, "minecraft/pack.json"); - Version game = JsonUtils.fromNonNullJson(gameJson, Version.class); - if (game.getJar() == null) + GameInstanceManifest game = JsonUtils.fromNonNullJson(gameJson, GameInstanceManifest.class); + if (game.jar() == null) if (StringUtils.isBlank(manifest.getVersion())) throw new JsonParseException("Cannot recognize the game version of modpack " + file + "."); else manifest.setManifest(HMCLModpackManifest.INSTANCE); else - manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.getJar()); + manifest.setManifest(HMCLModpackManifest.INSTANCE).setGameVersion(game.jar().id()); return manifest; } 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(), zipFile, this, name); + public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) { + return new HMCLModpackInstallTask((HMCLGameRepository) dependencyManager.getGameRepository(), zipFile, this, instanceId); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index 3e7ef722194..0b978df7808 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -84,7 +84,7 @@ public final class LauncherHelper { private final HMCLGameRepository repository; private Account account; - private final String selectedVersion; + private final GameInstanceID selectedInstanceId; private Path scriptFile; private final GameSettings.Effective setting; private LauncherVisibility launcherVisibility; @@ -92,14 +92,14 @@ public final class LauncherHelper { private QuickPlayOption quickPlayOption; private boolean disableOfflineSkin = false; - public LauncherHelper(HMCLGameRepository repository, Account account, String selectedVersion) { + public LauncherHelper(HMCLGameRepository repository, Account account, GameInstanceID selectedInstanceId) { this.repository = Objects.requireNonNull(repository); this.account = Objects.requireNonNull(account); - this.selectedVersion = Objects.requireNonNull(selectedVersion); - this.setting = repository.getEffectiveGameSettings(selectedVersion); + this.selectedInstanceId = selectedInstanceId; + this.setting = repository.getEffectiveGameSettings(selectedInstanceId); this.launcherVisibility = setting.getInheritable(GameSettings::launcherVisibilityProperty); this.showLogs = setting.getInheritable(GameSettings::showLogsProperty); - this.launchingStepsPane.setTitle(i18n("version.launch")); + this.launchingStepsPane.setTitle(i18n("instance.launch")); } private final TaskExecutorDialogPane launchingStepsPane = new TaskExecutorDialogPane(TaskCancellationAction.NORMAL); @@ -132,7 +132,7 @@ public void setDisableOfflineSkin() { public void launch() { FXUtils.checkFxUserThread(); - LOG.info("Launching game version: " + selectedVersion); + LOG.info("Launching game version: " + selectedInstanceId); Controllers.dialog(launchingStepsPane); launch0(); @@ -148,9 +148,9 @@ private void launch0() { PROCESSES.removeIf(it -> it.get() == null); DefaultDependencyManager dependencyManager = repository.getDependency(); - AtomicReference version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedVersion(selectedVersion))); + AtomicReference version = new AtomicReference<>(MaintainTask.maintain(repository, repository.getResolvedInstanceManifest(selectedInstanceId).launchManifest())); Optional gameVersion = repository.getGameVersion(version.get()); - boolean integrityCheck = repository.unmarkVersionLaunchedAbnormally(selectedVersion); + boolean integrityCheck = repository.unmarkInstanceLaunchedAbnormally(selectedInstanceId); CountDownLatch launchingLatch = new CountDownLatch(1); List javaAgents = new ArrayList<>(0); List javaArguments = new ArrayList<>(0); @@ -167,10 +167,10 @@ private void launch0() { dependencyManager.checkGameCompletionAsync(version.get(), integrityCheck), Task.composeAsync(() -> { try { - ModpackConfiguration configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedVersion)); + ModpackConfiguration configuration = ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(selectedInstanceId)); ModpackProvider provider = ModpackHelper.getProviderByType(configuration.getType()); if (provider == null) return null; - else return provider.createCompletionTask(dependencyManager, selectedVersion); + else return provider.createCompletionTask(dependencyManager, selectedInstanceId); } catch (IOException e) { return null; } @@ -202,7 +202,12 @@ private void launch0() { }) ); }).withStage("launch.state.dependencies") - .thenComposeAsync(() -> gameVersion.map(s -> new GameVerificationFixTask(dependencyManager, s, version.get())).orElse(null)) + .thenComposeAsync(() -> { + if (gameVersion.isEmpty()) { + return null; + } + return new GameVerificationFixTask(dependencyManager, gameVersion.get(), version.get()); + }) .thenComposeAsync(() -> { if (setting.getInheritable(GameSettings::allowAutoAgentProperty) || setting.getInheritable(GameSettings::noJVMOptionsProperty) @@ -228,7 +233,7 @@ private void launch0() { .thenComposeAsync(() -> logIn(account).withStage("launch.state.logging_in")) .thenComposeAsync(authInfo -> Task.supplyAsync(() -> { LaunchOptions.Builder launchOptionsBuilder = repository.getLaunchOptions( - selectedVersion, javaVersionRef.get(), repository.getBaseDirectory(), javaAgents, javaArguments, scriptFile != null); + selectedInstanceId, javaVersionRef.get(), repository.getBaseDirectory(), javaAgents, javaArguments, scriptFile != null); if (disableOfflineSkin) { launchOptionsBuilder.setDaemon(false); } @@ -238,7 +243,7 @@ private void launch0() { LaunchOptions launchOptions = launchOptionsBuilder.create(); - LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedVersion), 10)); + LOG.info("Here's the structure of game mod directory:\n" + FileUtils.printFileStructure(repository.getModsDirectory(selectedInstanceId), 10)); return new HMCLGameLauncher( repository, @@ -271,7 +276,7 @@ private void launch0() { } else { runLater(() -> { launchingStepsPane.fireEvent(new DialogCloseEvent()); - Controllers.dialog(i18n("version.launch_script.success", FileUtils.getAbsolutePath(scriptFile))); + Controllers.dialog(i18n("instance.launch_script.success", FileUtils.getAbsolutePath(scriptFile))); }); } }).withFakeProgress( @@ -373,7 +378,7 @@ public void onStop(boolean success, TaskExecutor executor) { message = StringUtils.getStackTrace(ex); } Controllers.dialog(message, - scriptFile == null ? i18n("launch.failed") : i18n("version.launch_script.failed"), + scriptFile == null ? i18n("launch.failed") : i18n("instance.launch_script.failed"), MessageType.ERROR); } } @@ -386,13 +391,13 @@ public void onStop(boolean success, TaskExecutor executor) { executor.start(); } - private static Task checkGameState(HMCLGameRepository repository, GameSettings.Effective setting, Version version) { - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null)); + private static Task checkGameState(HMCLGameRepository repository, GameSettings.Effective setting, GameInstanceManifest manifest) { + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(manifest, repository.getGameVersion(manifest).orElse(null)); GameVersionNumber gameVersion = GameVersionNumber.asGameVersion(analyzer.getVersion(LibraryAnalyzer.LibraryType.MINECRAFT)); Task getJavaTask = Task.supplyAsync(() -> { try { - return setting.getJava(gameVersion, version); + return setting.getJava(gameVersion, manifest); } catch (InterruptedException e) { throw new CancellationException(); } @@ -450,7 +455,7 @@ private static Task checkGameState(HMCLGameRepository repository, G } if (targetJavaVersion == null) - targetJavaVersion = version.getJavaVersion(); + targetJavaVersion = manifest.javaVersion(); } if (targetJavaVersion != null && supportedVersions.contains(targetJavaVersion)) { @@ -480,8 +485,8 @@ private static Task checkGameState(HMCLGameRepository repository, G if (java != null) { for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) { - if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) { - if (!constraint.checkJava(gameVersion, version, java, analyzer)) { + if (constraint.appliesToVersion(gameVersion, manifest, java, analyzer)) { + if (!constraint.checkJava(gameVersion, manifest, java, analyzer)) { if (constraint.isMandatory()) { violatedMandatoryConstraints.add(constraint); } else { @@ -497,7 +502,7 @@ private static Task checkGameState(HMCLGameRepository repository, G Runnable breakAction = () -> future.completeExceptionally(new CancellationException("Launch operation was cancelled by user")); if (java == null || !violatedMandatoryConstraints.isEmpty()) { - JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, version); + JavaRuntime suggestedJava = JavaManager.findSuitableJava(gameVersion, manifest); if (suggestedJava != null) { FXUtils.runInFX(() -> { Controllers.confirm(i18n("launch.advice.java.auto"), i18n("message.warning"), () -> { @@ -524,7 +529,7 @@ private static Task checkGameState(HMCLGameRepository repository, G ? GameJavaVersion.getCleanroomJavaVersion(cleanroomVersion) : GameJavaVersion.JAVA_21; } else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.GAME_JSON)) - gameJavaVersion = version.getJavaVersion(); + gameJavaVersion = manifest.javaVersion(); else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA)) gameJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersion); else @@ -650,14 +655,14 @@ else if (violatedMandatoryConstraints.contains(JavaVersionConstraint.VANILLA)) // Forge 2760~2773 will crash game with LiteLoader. boolean hasForge2760 = forgeVersion != null && (forgeVersion.compareTo("1.12.2-14.23.5.2760") >= 0) && (forgeVersion.compareTo("1.12.2-14.23.5.2773") < 0); - boolean hasLiteLoader = version.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader")); + boolean hasLiteLoader = manifest.getLibraries().stream().anyMatch(it -> it.is("com.mumfrey", "liteloader")); if (hasForge2760 && hasLiteLoader && gameVersion.compareTo("1.12.2") == 0) { suggestions.add(i18n("launch.advice.forge2760_liteloader")); } // OptiFine 1.14.4 is not compatible with Forge 28.2.2 and later versions. boolean hasForge28_2_2 = forgeVersion != null && (forgeVersion.compareTo("1.14.4-28.2.2") >= 0); - boolean hasOptiFine = version.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine")); + boolean hasOptiFine = manifest.getLibraries().stream().anyMatch(it -> it.is("optifine", "OptiFine")); if (hasForge28_2_2 && hasOptiFine && gameVersion.compareTo("1.14.4") == 0) { suggestions.add(i18n("launch.advice.forge28_2_2_optifine")); } @@ -798,7 +803,7 @@ private final class HMCLProcessListener implements ProcessListener { private final ReentrantLock lock = new ReentrantLock(); private final HMCLGameRepository repository; - private final Version version; + private final GameInstanceManifest manifest; private final LaunchOptions launchOptions; private ManagedProcess process; private volatile boolean lwjgl; @@ -810,9 +815,9 @@ private final class HMCLProcessListener implements ProcessListener { private Thread submitLogThread; private LinkedBlockingQueue logBuffer; - public HMCLProcessListener(HMCLGameRepository repository, Version version, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) { + public HMCLProcessListener(HMCLGameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions launchOptions, CountDownLatch launchingLatch, boolean detectWindow) { this.repository = repository; - this.version = version; + this.manifest = manifest; this.launchOptions = launchOptions; this.launchingLatch = launchingLatch; this.detectWindow = detectWindow; @@ -995,8 +1000,8 @@ public void onExit(int exitCode, ExitType exitType) { } if (exitType != ExitType.NORMAL) { - repository.markVersionLaunchedAbnormally(version.getId()); - runLater(() -> new GameCrashWindow(process, exitType, repository, version, launchOptions, logs).show()); + repository.markInstanceLaunchedAbnormally(manifest.id()); + runLater(() -> new GameCrashWindow(process, exitType, repository, manifest, launchOptions, logs).show()); } checkExit(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java index 3c531b0dd6a..08800ba861c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LocalizedRemoteAddonRepository.java @@ -20,7 +20,7 @@ import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.addon.RemoteAddonRepository; -import org.jackhuang.hmcl.ui.versions.ModTranslations; +import org.jackhuang.hmcl.ui.instances.ModTranslations; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java index 3ace4df057d..47ea469b3ce 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LogExporter.java @@ -17,7 +17,6 @@ */ package org.jackhuang.hmcl.game; -import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.IOUtils; import org.jackhuang.hmcl.util.io.Zipper; import org.jackhuang.hmcl.util.logging.Logger; @@ -42,22 +41,22 @@ private LogExporter() { } public static CompletableFuture exportLogs( - Path zipFile, DefaultGameRepository gameRepository, String versionId, String logs, String launchScript, + Path zipFile, DefaultGameRepository repository, GameInstanceID instanceId, String logs, String launchScript, PathMatcher logMatcher) { - Path runDirectory = gameRepository.getRunDirectory(versionId); - Path baseDirectory = gameRepository.getBaseDirectory(); - List versions = new ArrayList<>(); + Path runDirectory = repository.getRunDirectory(instanceId); + Path baseDirectory = repository.getBaseDirectory(); + List instances = new ArrayList<>(); - String currentVersionId = versionId; - HashSet resolvedSoFar = new HashSet<>(); + GameInstanceID currentInstanceId = instanceId; + HashSet resolvedSoFar = new HashSet<>(); while (true) { - if (resolvedSoFar.contains(currentVersionId)) break; - resolvedSoFar.add(currentVersionId); - Version currentVersion = gameRepository.getVersion(currentVersionId); - versions.add(currentVersionId); + if (resolvedSoFar.contains(currentInstanceId)) break; + resolvedSoFar.add(currentInstanceId); + GameInstanceManifest currentVersion = repository.getInstanceManifest(currentInstanceId); + instances.add(currentInstanceId); - if (StringUtils.isNotBlank(currentVersion.getInheritsFrom())) { - currentVersionId = currentVersion.getInheritsFrom(); + if (currentVersion.inheritsFrom() != null) { + currentInstanceId = currentVersion.inheritsFrom(); } else { break; } @@ -74,10 +73,10 @@ public static CompletableFuture exportLogs( zipper.putTextFile(logs, "minecraft.log"); zipper.putTextFile(Logger.filterForbiddenToken(launchScript), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "launch.bat" : "launch.sh"); - for (String id : versions) { - Path versionJson = baseDirectory.resolve("versions").resolve(id).resolve(id + ".json"); - if (Files.exists(versionJson)) { - zipper.putFile(versionJson, id + ".json"); + for (GameInstanceID id : instances) { + Path instanceJson = repository.getInstanceJson(id); + if (Files.exists(instanceJson)) { + zipper.putFile(instanceJson, id + ".json"); } } } catch (IOException e) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java index e84044e1b88..a8d600a26cb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/ModpackHelper.java @@ -156,13 +156,13 @@ public static ModpackConfiguration readModpackConfiguration(Path file) throws } } - public static Task getInstallTask(HMCLGameRepository repository, ServerModpackManifest manifest, String name, Modpack modpack) { - repository.markVersionAsModpack(name); + public static Task getInstallTask(HMCLGameRepository repository, ServerModpackManifest manifest, GameInstanceID instanceId, Modpack modpack) { + repository.markInstanceAsModpack(instanceId); ExceptionalRunnable success = () -> { - repository.refreshVersions(); - GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(name); - repository.undoMark(name); + repository.refresh(); + GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId); + repository.undoMark(instanceId); if (setting != null) { setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY); } @@ -175,7 +175,7 @@ public static Task getInstallTask(HMCLGameRepository repository, ServerModpac } }; - return new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, name) + return new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instanceId) .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"))); } @@ -200,13 +200,13 @@ public static Task getInstallManuallyCreatedModpackTask(Path zipFile, String }); } - public static Task getInstallTask(HMCLGameRepository repository, Path zipFile, String name, Modpack modpack, String iconUrl) { - repository.markVersionAsModpack(name); + public static Task getInstallTask(HMCLGameRepository repository, Path zipFile, GameInstanceID instanceId, Modpack modpack, String iconUrl) { + repository.markInstanceAsModpack(instanceId); ExceptionalRunnable success = () -> { - repository.refreshVersions(); - GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(name); - repository.undoMark(name); + repository.refresh(); + GameSettings.Instance setting = repository.getInstanceGameSettingsOrCreate(instanceId); + repository.undoMark(instanceId); if (setting != null) { setting.getOverrideProperties().add(GameSettings.PROPERTY_RUNNING_DIRECTORY); } @@ -220,45 +220,45 @@ public static Task getInstallTask(HMCLGameRepository repository, Path zipFile }; if (modpack.getManifest() instanceof MultiMCInstanceConfiguration) - return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl) + return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl) .whenComplete(Schedulers.defaultScheduler(), success, failure) - .thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), name)) + .thenComposeAsync(createMultiMCPostInstallTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId)) .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(repository.getDependency(), zipFile, name, iconUrl) + return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, iconUrl) .whenComplete(Schedulers.defaultScheduler(), success, failure) - .thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), name)) + .thenComposeAsync(createMcbbsPostInstallTask(repository, (McbbsModpackManifest) modpack.getManifest(), instanceId)) .withStagesHints(new Task.StagesHint("hmcl.modpack"), new Task.StagesHint("hmcl.modpack.download", List.of("hmcl.install.assets", "hmcl.install.libraries"))); else - return modpack.getInstallTask(repository.getDependency(), zipFile, name, iconUrl) + return modpack.getInstallTask(repository.getDependency(), zipFile, instanceId, 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 getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, Charset charset, String name, ModpackConfiguration configuration) throws UnsupportedModpackException { + public static Task getUpdateTask(HMCLGameRepository repository, ServerModpackManifest manifest, Charset charset, GameInstanceID instanceId, ModpackConfiguration configuration) throws UnsupportedModpackException { switch (configuration.getType()) { case ServerModpackRemoteInstallTask.MODPACK_TYPE: - return new ModpackUpdateTask(repository, name, new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, name)) - .thenComposeAsync(repository.refreshVersionsAsync()) + return new ModpackUpdateTask(repository, instanceId, new ServerModpackRemoteInstallTask(repository.getDependency(), manifest, instanceId)) + .thenComposeAsync(repository.refreshAsync()) .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(HMCLGameRepository repository, Path zipFile, Charset charset, String name, ModpackConfiguration configuration) throws UnsupportedModpackException, ManuallyCreatedModpackException, MismatchedModpackTypeException { + public static Task getUpdateTask(HMCLGameRepository repository, Path zipFile, Charset charset, GameInstanceID instanceId, 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(repository.getDependency(), name, zipFile, modpack) - .thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), name)) - .thenComposeAsync(repository.refreshVersionsAsync()); + return provider.createUpdateTask(repository.getDependency(), instanceId, zipFile, modpack) + .thenComposeAsync(() -> createMultiMCPostUpdateTask(repository, (MultiMCInstanceConfiguration) modpack.getManifest(), instanceId)) + .thenComposeAsync(repository.refreshAsync()); else - return provider.createUpdateTask(repository.getDependency(), name, zipFile, modpack) - .thenComposeAsync(repository.refreshVersionsAsync()); + return provider.createUpdateTask(repository.getDependency(), instanceId, zipFile, modpack) + .thenComposeAsync(repository.refreshAsync()); } public static void toGameSettings(MultiMCInstanceConfiguration c, GameSettings.Instance setting) { @@ -333,25 +333,25 @@ private static void applyCommandAndJvmSettings(MultiMCInstanceConfiguration c, G } } - private static Task createMultiMCPostUpdateTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, String version) { + private static Task createMultiMCPostUpdateTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, GameInstanceID instanceId) { return Task.runAsync(Schedulers.javafx(), () -> { - GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(version)); + GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(instanceId)); ModpackHelper.applyCommandAndJvmSettings(manifest, setting); }); } - private static Task createMultiMCPostInstallTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, String version) { + private static Task createMultiMCPostInstallTask(HMCLGameRepository repository, MultiMCInstanceConfiguration manifest, GameInstanceID instanceId) { return Task.runAsync(Schedulers.javafx(), () -> { - GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(version)); + GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(instanceId)); ModpackHelper.toGameSettings(manifest, setting); }); } - private static Task createMcbbsPostInstallTask(HMCLGameRepository repository, McbbsModpackManifest manifest, String version) { + private static Task createMcbbsPostInstallTask(HMCLGameRepository repository, McbbsModpackManifest manifest, GameInstanceID instanceId) { return Task.runAsync(Schedulers.javafx(), () -> { - GameSettings.Effective effective = repository.getEffectiveGameSettings(version); + GameSettings.Effective effective = repository.getEffectiveGameSettings(instanceId); if (manifest.getLaunchInfo().getMinMemory() > effective.getMaxMemory()) { - GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(version)); + GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettingsOrCreate(instanceId)); setting.getOverrideProperties().addAll(List.of( GameSettings.PROPERTY_AUTO_MEMORY, GameSettings.PROPERTY_MIN_MEMORY, diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java index 3d1c11b8bb4..96b33964e9b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/java/JavaManager.java @@ -29,7 +29,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.GameJavaVersion; import org.jackhuang.hmcl.game.JavaVersionConstraint; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; @@ -315,13 +315,13 @@ private static JavaRuntime chooseJava(@Nullable JavaRuntime java1, JavaRuntime j } @Nullable - public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, Version version) throws InterruptedException { - return findSuitableJava(getAllJava(), gameVersion, version); + public static JavaRuntime findSuitableJava(GameVersionNumber gameVersion, GameInstanceManifest manifest) throws InterruptedException { + return findSuitableJava(getAllJava(), gameVersion, manifest); } @Nullable - public static JavaRuntime findSuitableJava(Collection javaRuntimes, GameVersionNumber gameVersion, Version version) { - LibraryAnalyzer analyzer = version != null ? LibraryAnalyzer.analyze(version, gameVersion != null ? gameVersion.toString() : null) : null; + public static JavaRuntime findSuitableJava(Collection javaRuntimes, GameVersionNumber gameVersion, GameInstanceManifest manifest) { + LibraryAnalyzer analyzer = manifest != null ? LibraryAnalyzer.analyze(manifest, gameVersion != null ? gameVersion.toString() : null) : null; boolean forceX86 = Architecture.SYSTEM_ARCH == Architecture.ARM64 && (OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS || OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) @@ -342,8 +342,8 @@ public static JavaRuntime findSuitableJava(Collection javaRuntimes, boolean violationSuggested = false; for (JavaVersionConstraint constraint : JavaVersionConstraint.ALL) { - if (constraint.appliesToVersion(gameVersion, version, java, analyzer)) { - if (!constraint.checkJava(gameVersion, version, java, analyzer)) { + if (constraint.appliesToVersion(gameVersion, manifest, java, analyzer)) { + if (!constraint.checkJava(gameVersion, manifest, java, analyzer)) { if (constraint.isMandatory()) { violationMandatory = true; } else { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameDirectoryManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameDirectoryManager.java index 58b89394286..4e0ac50efc8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameDirectoryManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameDirectoryManager.java @@ -23,7 +23,8 @@ 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.event.RefreshedGameInstancesEvent; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.util.PortablePath; import org.jackhuang.hmcl.util.i18n.I18n; @@ -132,10 +133,10 @@ private static boolean isGameDirectoryPath(GameDirectory gameDirectory, Portable 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"); + private static final ReadOnlyObjectWrapper selectedInstance = new ReadOnlyObjectWrapper<>(GameDirectoryManager.class, "selectedInstance"); /// Updates [#selectedInstance] when the selected repository changes its selected instance. - private static final ChangeListener selectedRepositoryInstanceListener = + private static final ChangeListener selectedRepositoryInstanceListener = (observable, oldValue, newValue) -> selectedInstance.set(newValue); /// Initializes game directory state from the stores loaded by [SettingsManager]. @@ -195,11 +196,11 @@ public static void init() { selectedRepository.set(repository); selectedInstance.set(repository.getSelectedInstance()); repository.selectedInstanceProperty().addListener(selectedRepositoryInstanceListener); - repository.refreshVersionsAsync().start(); + repository.refreshAsync().start(); }); selectedGameDirectory.set(currentGameDirectory != null ? currentGameDirectory : mergedGameDirectories.get(0)); - EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> { + EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class).registerWeak(event -> { runInFX(() -> { @Nullable HMCLGameRepository repository = selectedRepository.get(); if (repository != null && repository == event.getSource()) { @@ -466,17 +467,17 @@ public static ObjectProperty selectedRepositoryProperty() { } /// Returns the selected instance property projected from the selected repository. - public static ReadOnlyStringProperty selectedInstanceProperty() { + public static ReadOnlyObjectProperty<@Nullable GameInstanceID> selectedInstanceProperty() { return selectedInstance.getReadOnlyProperty(); } /// Returns the selected instance ID for the selected repository. - public static @Nullable String getSelectedInstance() { + public static @Nullable GameInstanceID getSelectedInstance() { return getSelectedRepository().getSelectedInstance(); } /// Sets the selected instance ID for the selected repository. - public static void setSelectedInstance(@Nullable String instance) { + public static void setSelectedInstance(@Nullable GameInstanceID instance) { getSelectedRepository().setSelectedInstance(instance); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameInstanceIconType.java similarity index 76% rename from HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java rename to HMCL/src/main/java/org/jackhuang/hmcl/setting/GameInstanceIconType.java index ed8a24cd9d5..369bd815ec1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionIconType.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameInstanceIconType.java @@ -21,7 +21,7 @@ import org.jackhuang.hmcl.addon.mod.ModLoaderType; import org.jackhuang.hmcl.ui.FXUtils; -public enum VersionIconType { +public enum GameInstanceIconType { DEFAULT("/assets/img/grass.png"), GRASS("/assets/img/grass.png"), @@ -42,21 +42,21 @@ public enum VersionIconType { // Please append new items at last - public static VersionIconType getIconType(ModLoaderType modLoaderType) { + public static GameInstanceIconType getIconType(ModLoaderType modLoaderType) { return switch (modLoaderType) { - case FORGE -> VersionIconType.FORGE; - case NEO_FORGE -> VersionIconType.NEO_FORGE; - case FABRIC -> VersionIconType.FABRIC; - case QUILT -> VersionIconType.QUILT; - case LITE_LOADER -> VersionIconType.CHICKEN; - case CLEANROOM -> VersionIconType.CLEANROOM; - default -> VersionIconType.COMMAND; + case FORGE -> GameInstanceIconType.FORGE; + case NEO_FORGE -> GameInstanceIconType.NEO_FORGE; + case FABRIC -> GameInstanceIconType.FABRIC; + case QUILT -> GameInstanceIconType.QUILT; + case LITE_LOADER -> GameInstanceIconType.CHICKEN; + case CLEANROOM -> GameInstanceIconType.CLEANROOM; + default -> GameInstanceIconType.COMMAND; }; } private final String resourceUrl; - VersionIconType(String resourceUrl) { + GameInstanceIconType(String resourceUrl) { this.resourceUrl = resourceUrl; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java index 81fdd30229e..20e7fa5f5b1 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GameSettings.java @@ -152,10 +152,10 @@ public void setBackupOnNextSave(boolean backupOnNextSave) { /// The icon of the instance. @SerializedName("icon") - private final SettingProperty icon = newSettingProperty("icon", VersionIconType.DEFAULT); + private final SettingProperty icon = newSettingProperty("icon", GameInstanceIconType.DEFAULT); /// Returns the instance icon property. - public SettingProperty iconProperty() { + public SettingProperty iconProperty() { return icon; } @@ -916,11 +916,11 @@ public void setJavaAutoSelected() { } /// Finds the effective Java runtime. - public @Nullable JavaRuntime getJava(@Nullable GameVersionNumber gameVersion, @Nullable Version version) throws InterruptedException { + public @Nullable JavaRuntime getJava(@Nullable GameVersionNumber gameVersion, @Nullable GameInstanceManifest manifest) throws InterruptedException { JavaVersionType javaVersionType = getInheritable(GameSettings::javaTypeProperty); switch (javaVersionType) { case AUTO: - return JavaManager.findSuitableJava(gameVersion, version); + return JavaManager.findSuitableJava(gameVersion, manifest); case CUSTOM: try { return JavaManager.getJava(Path.of(getInheritable(GameSettings::customJavaPathProperty))); @@ -930,7 +930,7 @@ public void setJavaAutoSelected() { case VERSION: { String javaVersion = getInheritable(GameSettings::customJavaVersionProperty); if (StringUtils.isBlank(javaVersion)) { - return JavaManager.findSuitableJava(gameVersion, version); + return JavaManager.findSuitableJava(gameVersion, manifest); } int majorVersion = -1; @@ -948,13 +948,13 @@ public void setJavaAutoSelected() { Collection allJava = JavaManager.getAllJava().stream() .filter(it -> it.getParsedVersion() == finalMajorVersion) .collect(Collectors.toList()); - return JavaManager.findSuitableJava(allJava, gameVersion, version); + return JavaManager.findSuitableJava(allJava, gameVersion, manifest); } case DETECTED: { DetectedJava detectedJava = getInheritable(GameSettings::detectedJavaProperty); String javaVersion = detectedJava.version(); if (StringUtils.isBlank(javaVersion)) { - return JavaManager.findSuitableJava(gameVersion, version); + return JavaManager.findSuitableJava(gameVersion, manifest); } if (StringUtils.isNotBlank(detectedJava.pathHash())) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherSettings.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherSettings.java index 81028e69363..098aedfe987 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherSettings.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LauncherSettings.java @@ -34,6 +34,7 @@ import org.hildan.fxgson.factories.JavaFxPropertyTypeAdapterFactory; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.auth.AccountID; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.java.JavaRuntime; import org.jackhuang.hmcl.theme.BackgroundLoadPolicy; import org.jackhuang.hmcl.theme.BuiltinBackground; @@ -609,34 +610,34 @@ public StringProperty proxyPasswordProperty() { /// /// This field is owned by [GameDirectoryManager]. Code outside [GameDirectoryManager] should not modify it directly. @SerializedName(PROPERTY_SELECTED_INSTANCE) - private final ObservableMap selectedInstance = FXCollections.observableHashMap(); + private final ObservableMap selectedInstance = FXCollections.observableHashMap(); /// Returns selected instance IDs keyed by game directory ID. /// /// The map stores persisted selected instance values by game directory ID. - public ObservableMap getSelectedInstance() { + public ObservableMap getSelectedInstance() { return selectedInstance; } /// Returns the selected instance ID for the given game directory ID. /// /// The value is loaded by the game repository for the matching game directory. - public @Nullable String getSelectedInstance(@Nullable GameDirectoryID gameDirectoryId) { + public @Nullable GameInstanceID getSelectedInstance(@Nullable GameDirectoryID gameDirectoryId) { return gameDirectoryId != null ? selectedInstance.get(gameDirectoryId) : null; } /// Sets the selected instance ID for the given game directory ID. /// /// Blank values remove the persisted selected instance entry. - public void setSelectedInstance(@Nullable GameDirectoryID gameDirectoryId, @Nullable String selectedInstance) { + public void setSelectedInstance(@Nullable GameDirectoryID gameDirectoryId, @Nullable GameInstanceID selectedInstance) { if (gameDirectoryId == null) { return; } - if (StringUtils.isBlank(selectedInstance)) { - this.selectedInstance.remove(gameDirectoryId); - } else { + if (selectedInstance != null) { this.selectedInstance.put(gameDirectoryId, selectedInstance); + } else { + this.selectedInstance.remove(gameDirectoryId); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/LegacyGameSettingsMigrator.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LegacyGameSettingsMigrator.java index 16dea2450f9..3b34a007645 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/LegacyGameSettingsMigrator.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/LegacyGameSettingsMigrator.java @@ -73,22 +73,22 @@ private enum GameDirectoryType { } /// Legacy `VersionIconType` ordinal order used by old local settings. - private static final VersionIconType @Unmodifiable [] LEGACY_VERSION_ICON_TYPES = { - VersionIconType.DEFAULT, - VersionIconType.GRASS, - VersionIconType.CHEST, - VersionIconType.CHICKEN, - VersionIconType.COMMAND, - VersionIconType.OPTIFINE, - VersionIconType.CRAFT_TABLE, - VersionIconType.FABRIC, - VersionIconType.FORGE, - VersionIconType.NEO_FORGE, - VersionIconType.FURNACE, - VersionIconType.QUILT, - VersionIconType.APRIL_FOOLS, - VersionIconType.CLEANROOM, - VersionIconType.LEGACY_FABRIC + private static final GameInstanceIconType @Unmodifiable [] LEGACY_VERSION_ICON_TYPES = { + GameInstanceIconType.DEFAULT, + GameInstanceIconType.GRASS, + GameInstanceIconType.CHEST, + GameInstanceIconType.CHICKEN, + GameInstanceIconType.COMMAND, + GameInstanceIconType.OPTIFINE, + GameInstanceIconType.CRAFT_TABLE, + GameInstanceIconType.FABRIC, + GameInstanceIconType.FORGE, + GameInstanceIconType.NEO_FORGE, + GameInstanceIconType.FURNACE, + GameInstanceIconType.QUILT, + GameInstanceIconType.APRIL_FOOLS, + GameInstanceIconType.CLEANROOM, + GameInstanceIconType.LEGACY_FABRIC }; /// Legacy `JavaVersionType` ordinal order used by old game settings. @@ -130,9 +130,9 @@ public static GameSettings.Preset toPreset(GameSettingsPresetID id, int autoName /// @return the migrated instance setting, or `null` when no legacy file can be migrated public static @Nullable InstanceMigrationResult migrateInstanceGameSettings( HMCLGameRepository repository, - String instanceId, + GameInstanceID instanceId, @Nullable GameSettingsPresetID parent) { - Path instanceRoot = repository.getVersionRoot(instanceId); + Path instanceRoot = repository.getInstanceRoot(instanceId); Path file = instanceRoot.resolve(LEGACY_INSTANCE_SETTINGS_FILENAME); if (!Files.exists(file)) { return null; @@ -488,10 +488,10 @@ private static GraphicsAPI parseGraphicsBackend(JsonObject source, Renderer rend } /// Parses the legacy icon selection with frozen ordinal order. - private static VersionIconType parseLegacyVersionIconType(@Nullable JsonObject source) { + private static GameInstanceIconType parseLegacyVersionIconType(@Nullable JsonObject source) { JsonPrimitive primitive = JsonUtils.getPrimitive(source, "versionIcon"); if (primitive == null) { - return VersionIconType.DEFAULT; + return GameInstanceIconType.DEFAULT; } try { @@ -499,11 +499,11 @@ private static VersionIconType parseLegacyVersionIconType(@Nullable JsonObject s int index = primitive.getAsInt(); return index >= 0 && index < LEGACY_VERSION_ICON_TYPES.length ? LEGACY_VERSION_ICON_TYPES[index] - : VersionIconType.DEFAULT; + : GameInstanceIconType.DEFAULT; } String value = primitive.getAsString(); - for (VersionIconType iconType : LEGACY_VERSION_ICON_TYPES) { + for (GameInstanceIconType iconType : LEGACY_VERSION_ICON_TYPES) { if (iconType.name().equalsIgnoreCase(value)) { return iconType; } @@ -511,7 +511,7 @@ private static VersionIconType parseLegacyVersionIconType(@Nullable JsonObject s } catch (RuntimeException ignored) { } - return VersionIconType.DEFAULT; + return GameInstanceIconType.DEFAULT; } /// Reads an enum property from either ordinal or name. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java index 0c70fed237d..2a1b68d163b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -59,9 +59,9 @@ import org.jackhuang.hmcl.ui.main.LauncherSettingsPage; import org.jackhuang.hmcl.ui.main.RootPage; import org.jackhuang.hmcl.ui.terracotta.TerracottaPage; -import org.jackhuang.hmcl.ui.versions.GameListPage; -import org.jackhuang.hmcl.ui.versions.VersionPage; -import org.jackhuang.hmcl.ui.versions.Versions; +import org.jackhuang.hmcl.ui.instances.GameListPage; +import org.jackhuang.hmcl.ui.instances.GameInstancePage; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.util.*; import org.jackhuang.hmcl.util.i18n.I18n; @@ -106,7 +106,7 @@ public final class Controllers { private static Scene scene; private static Stage stage; - private static VersionPage versionPage; + private static GameInstancePage gameInstancePage; private static Lazy gameListPage = new Lazy<>(GameListPage::new); private static Lazy rootPage = new Lazy<>(RootPage::new); private static DecoratorController decorator; @@ -142,18 +142,18 @@ public static Stage getStage() { } @FXThread - public static VersionPage getVersionPage() { - if (versionPage == null) { - versionPage = new VersionPage(); + public static GameInstancePage getGameInstancePage() { + if (gameInstancePage == null) { + gameInstancePage = new GameInstancePage(); } - return versionPage; + return gameInstancePage; } @FXThread - public static void prepareVersionPage() { - if (versionPage == null) { - LOG.info("Prepare the version page"); - versionPage = FXUtils.prepareNode(new VersionPage()); + public static void prepareGameInstancePage() { + if (gameInstancePage == null) { + LOG.info("Prepare the game instance page"); + gameInstancePage = FXUtils.prepareNode(new GameInstancePage()); } } @@ -703,7 +703,7 @@ public static void onHyperlinkAction(String href) { break; case "hmcl://game/launch": var repository = GameDirectoryManager.getSelectedRepository(); - Versions.launch(repository, repository.getSelectedInstance(), LauncherHelper::setKeep); + Instances.launch(repository, repository.getSelectedInstance(), LauncherHelper::setKeep); break; } } else { @@ -717,7 +717,7 @@ public static boolean isStopped() { public static void shutdown() { rootPage = null; - versionPage = null; + gameInstancePage = null; gameListPage = null; downloadPage = null; accountListPage = null; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java index 1558ee62959..5037480bfe6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/GameCrashWindow.java @@ -73,7 +73,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public class GameCrashWindow extends Stage { - private final Version version; + private final GameInstanceManifest manifest; private final String memory; private final String total_memory; private final String java; @@ -91,16 +91,16 @@ public class GameCrashWindow extends Stage { private final List logs; - public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, Version version, LaunchOptions launchOptions, List logs) { + public GameCrashWindow(ManagedProcess managedProcess, ProcessListener.ExitType exitType, DefaultGameRepository repository, GameInstanceManifest manifest, LaunchOptions launchOptions, List logs) { Themes.applyNativeDarkMode(this); this.managedProcess = managedProcess; this.exitType = exitType; this.repository = repository; - this.version = version; + this.manifest = manifest; this.launchOptions = launchOptions; this.logs = logs; - this.analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null)); + this.analyzer = LibraryAnalyzer.analyze(manifest, repository.getGameVersion(manifest).orElse(null)); memory = Optional.ofNullable(launchOptions.getMaxMemory()).map(i -> i + " " + i18n("settings.memory.unit.mib")).orElse("-"); @@ -142,7 +142,7 @@ private void analyzeCrashReport() { return pair(CrashReportAnalyzer.analyze(rawLog), crashReport != null ? CrashReportAnalyzer.findKeywordsFromCrashReport(crashReport) : new HashSet<>()); }), Task.supplyAsync(() -> { - Path latestLog = repository.getRunDirectory(version.getId()).resolve("logs/latest.log"); + Path latestLog = repository.getRunDirectory(manifest.id()).resolve("logs/latest.log"); if (!Files.isReadable(latestLog)) { return pair(new HashSet(), new HashSet()); } @@ -291,7 +291,7 @@ private CompletableFuture exportGameCrashInfo() { } }); - return LogExporter.exportLogs(logFile, repository, launchOptions.getVersionName(), logs, + return LogExporter.exportLogs(logFile, repository, launchOptions.getInstanceId(), logs, new CommandBuilder().addAll(managedProcess.getCommands()).toString(), path -> { try { @@ -345,7 +345,7 @@ private final class View extends VBox { TwoLineListItem version = new TwoLineListItem(); version.getStyleClass().setAll("two-line-item-second-large"); version.setTitle(i18n("game.version")); - version.setSubtitle(GameCrashWindow.this.version.getId()); + version.setSubtitle(GameCrashWindow.this.manifest.id().toString()); TwoLineListItem total_memory = new TwoLineListItem(); total_memory.getStyleClass().setAll("two-line-item-second-large"); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java index b9d08f897d7..a9938e7dece 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/InstallerItem.java @@ -35,7 +35,7 @@ import javafx.scene.input.MouseButton; import javafx.scene.layout.*; import org.jackhuang.hmcl.download.LibraryAnalyzer; -import org.jackhuang.hmcl.setting.VersionIconType; +import org.jackhuang.hmcl.setting.GameInstanceIconType; import org.jackhuang.hmcl.ui.construct.ImageContainer; import org.jackhuang.hmcl.ui.construct.RipplerContainer; import org.jackhuang.hmcl.util.i18n.I18n; @@ -54,7 +54,7 @@ */ public class InstallerItem extends Control { private final String id; - private final VersionIconType iconType; + private final GameInstanceIconType iconType; private final Style style; private final ObjectProperty versionProperty = new SimpleObjectProperty<>(this, "version", null); private final ObjectProperty resolvedStateProperty = new SimpleObjectProperty<>(this, "resolvedState", InstallableState.INSTANCE); @@ -92,15 +92,15 @@ public InstallerItem(String id, Style style) { this.style = style; iconType = switch (id) { - case "game" -> VersionIconType.GRASS; - case "fabric", "fabric-api" -> VersionIconType.FABRIC; - case "legacyfabric", "legacyfabric-api" -> VersionIconType.LEGACY_FABRIC; - case "forge" -> VersionIconType.FORGE; - case "cleanroom" -> VersionIconType.CLEANROOM; - case "liteloader" -> VersionIconType.CHICKEN; - case "optifine" -> VersionIconType.OPTIFINE; - case "quilt", "quilt-api" -> VersionIconType.QUILT; - case "neoforge" -> VersionIconType.NEO_FORGE; + case "game" -> GameInstanceIconType.GRASS; + case "fabric", "fabric-api" -> GameInstanceIconType.FABRIC; + case "legacyfabric", "legacyfabric-api" -> GameInstanceIconType.LEGACY_FABRIC; + case "forge" -> GameInstanceIconType.FORGE; + case "cleanroom" -> GameInstanceIconType.CLEANROOM; + case "liteloader" -> GameInstanceIconType.CHICKEN; + case "optifine" -> GameInstanceIconType.OPTIFINE; + case "quilt", "quilt-api" -> GameInstanceIconType.QUILT; + case "neoforge" -> GameInstanceIconType.NEO_FORGE; default -> null; }; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AbstractInstallersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AbstractInstallersPage.java index b1c2a97d590..f5823405239 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AbstractInstallersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AbstractInstallersPage.java @@ -131,7 +131,7 @@ protected InstallersPageSkin(AbstractInstallersPage control) { versionNamePane.setAlignment(Pos.CENTER_LEFT); control.txtName.setMaxWidth(300); - versionNamePane.getChildren().setAll(new Label(i18n("version.name")), control.txtName); + versionNamePane.getChildren().setAll(new Label(i18n("instance.name")), control.txtName); root.setTop(versionNamePane); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AdditionalInstallersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AdditionalInstallersPage.java index 7464556d7d6..c5b0354425b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AdditionalInstallersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/AdditionalInstallersPage.java @@ -23,9 +23,8 @@ import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.GameRepository; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.ui.InstallerItem; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.util.Lang; @@ -38,17 +37,17 @@ class AdditionalInstallersPage extends AbstractInstallersPage { protected final BooleanProperty compatible = new SimpleBooleanProperty(); - protected final GameRepository repository; + protected final HMCLGameRepository repository; protected final String gameVersion; - protected final Version version; + protected final GameInstanceManifest manifest; - public AdditionalInstallersPage(String gameVersion, Version version, WizardController controller, HMCLGameRepository repository, DownloadProvider downloadProvider) { + public AdditionalInstallersPage(String gameVersion, GameInstanceManifest manifest, WizardController controller, HMCLGameRepository repository, DownloadProvider downloadProvider) { super(controller, gameVersion, downloadProvider); this.gameVersion = gameVersion; - this.version = version; + this.manifest = manifest; this.repository = repository; - txtName.setText(version.getId()); + txtName.setText(manifest.id().toString()); txtName.setEditable(false); for (InstallerItem library : group.getLibraries()) { @@ -81,8 +80,8 @@ private String getVersion(String id) { @Override protected void reload() { - Version resolved = version.resolvePreservingPatches(repository); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, repository.getGameVersion(resolved).orElse(null)); + GameInstanceManifest.Resolved resolvedManifest = repository.resolve(manifest); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolvedManifest, repository.getGameVersion(manifest).orElse(null)); String game = analyzer.getVersion(MINECRAFT).orElse(null); String currentGameVersion = Lang.nonNull(getVersion("game"), game); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java index 58a3cc5fc3a..a00e0f864ae 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/DownloadPage.java @@ -25,6 +25,7 @@ import org.jackhuang.hmcl.addon.repository.CurseForgeRemoteAddonRepository; import org.jackhuang.hmcl.download.*; import org.jackhuang.hmcl.download.game.GameRemoteVersion; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.GameDirectoryManager; @@ -42,10 +43,10 @@ import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.ui.versions.DownloadListPage; -import org.jackhuang.hmcl.ui.versions.HMCLLocalizedDownloadListPage; -import org.jackhuang.hmcl.ui.versions.VersionPage; -import org.jackhuang.hmcl.ui.versions.Versions; +import org.jackhuang.hmcl.ui.instances.DownloadListPage; +import org.jackhuang.hmcl.ui.instances.HMCLLocalizedDownloadListPage; +import org.jackhuang.hmcl.ui.instances.GameInstancePage; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.ui.wizard.Navigation; import org.jackhuang.hmcl.ui.wizard.WizardController; import org.jackhuang.hmcl.ui.wizard.WizardProvider; @@ -68,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 = + public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_MOD = (downloadProvider, repository, version, mod, file) -> download(downloadProvider, repository, version, file, "mods"); - public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_RESOURCE_PACK = + public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_RESOURCE_PACK = (downloadProvider, repository, version, pack, file) -> download(downloadProvider, repository, version, file, "resourcepacks"); - public static final org.jackhuang.hmcl.ui.versions.DownloadPage.DownloadCallback FOR_SHADER = + public static final org.jackhuang.hmcl.ui.instances.DownloadPage.DownloadCallback FOR_SHADER = (downloadProvider, repository, version, shader, file) -> download(downloadProvider, repository, version, file, "shaderpacks"); private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(DecoratorPage.State.fromTitle(i18n("download"), -1)); @@ -92,16 +93,16 @@ public DownloadPage() { this(null); } - public DownloadPage(String uploadVersion) { + public DownloadPage(GameInstanceID uploadInstance) { 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, repository, __, modpack, file) -> { - Versions.downloadModpackImpl(downloadProvider, repository, uploadVersion, modpack, file); + Instances.downloadModpackImpl(downloadProvider, repository, uploadInstance, modpack, file); }, false); JFXButton installLocalModpackButton = FXUtils.newRaisedButton(i18n("install.modpack")); - installLocalModpackButton.setOnAction(e -> Versions.importModpack()); + installLocalModpackButton.setOnAction(e -> Instances.importModpack()); page.getActions().add(installLocalModpackButton); return page; @@ -134,17 +135,19 @@ public DownloadPage(String uploadVersion) { private static Supplier loadVersionFor(Supplier nodeSupplier) { return () -> { T node = nodeSupplier.get(); - if (node instanceof VersionPage.GameInstanceLoadable loadable) { + if (node instanceof GameInstancePage.GameInstanceLoadable loadable) { loadable.loadInstance(GameDirectoryManager.getSelectedRepository(), null); } return node; }; } - public static void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable String version, RemoteAddon.Version file, String subdirectoryName) { - if (version == null) version = repository.getSelectedInstance(); + public static void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable GameInstanceID instanceId, RemoteAddon.Version file, String subdirectoryName) { + if (instanceId == null) { + instanceId = repository.getSelectedInstance(); + } - Path runDirectory = repository.hasVersion(version) ? repository.getRunDirectory(version) : repository.getBaseDirectory(); + Path runDirectory = instanceId != null && repository.hasInstance(instanceId) ? repository.getRunDirectory(instanceId) : repository.getBaseDirectory(); Set existingFiles; @@ -306,10 +309,11 @@ public void start(SettingsMap settings) { } private Task finishVersionDownloadingAsync(SettingsMap settings) { - GameBuilder builder = dependencyManager.gameBuilder(); + GameBuilder builder = dependencyManager.newGameBuilder(); String name = (String) settings.get("name"); - builder.name(name); + GameInstanceID instanceId = new GameInstanceID(name); + builder.name(instanceId); builder.gameVersion(((RemoteVersion) settings.get(LibraryAnalyzer.LibraryType.MINECRAFT.getPatchId())).getGameVersion()); settings.asStringMap().forEach((key, value) -> { @@ -319,9 +323,9 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { }); return builder.buildAsync().whenComplete(any -> { - repository.refreshVersions(); - repository.applyDefaultIsolationSetting(name); - }).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name)); + repository.refresh(); + repository.applyDefaultIsolationSetting(instanceId); + }).thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(instanceId)); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java index 3d07ed74b61..7678129b224 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/InstallersPage.java @@ -42,8 +42,8 @@ public InstallersPage(WizardController controller, HMCLGameRepository repository txtName.getValidators().addAll( new RequiredValidator(), - new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)), - new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId)); + new Validator(i18n("install.new_game.already_exists"), str -> !repository.instanceIdConflicts(str)), + new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId)); installable.bind(createBooleanBinding(txtName::validate, txtName.textProperty())); txtName.textProperty().addListener((obs, oldText, newText) -> isNameModifiedByUser = true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java index c2275a0d9bb..39f92f5cc03 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/LocalModpackPage.java @@ -66,8 +66,8 @@ public LocalModpackPage(WizardController controller) { if (installAsVersion) { txtModpackName.getValidators().setAll( new RequiredValidator(), - new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)), - new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId)); + new Validator(i18n("install.new_game.already_exists"), str -> !repository.instanceIdConflicts(str)), + new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId)); } else { txtModpackName.getValidators().setAll( new RequiredValidator(), @@ -75,7 +75,7 @@ public LocalModpackPage(WizardController controller) { && GameDirectoryManager.getGameDirectories().stream() .noneMatch(existingProfile -> str.equals(GameDirectoryManager.getGameDirectoryCustomName(existingProfile)))), - new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId)); + new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId)); } }); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java index 21f0e512b98..9a56fbc1320 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackInstallWizardProvider.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.download; import javafx.scene.Node; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.game.ManuallyCreatedModpackException; import org.jackhuang.hmcl.game.ModpackHelper; @@ -57,14 +58,14 @@ public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackF this(repository, modpackFile, null); } - public ModpackInstallWizardProvider(HMCLGameRepository repository, String updateVersion) { - this(repository, null, updateVersion); + public ModpackInstallWizardProvider(HMCLGameRepository repository, GameInstanceID updateInstanceId) { + this(repository, null, updateInstanceId); } - public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile, String updateVersion) { + public ModpackInstallWizardProvider(HMCLGameRepository repository, Path modpackFile, GameInstanceID updateInstanceId) { this.repository = repository; this.file = modpackFile; - this.updateVersion = updateVersion; + this.updateVersion = updateInstanceId != null ? updateInstanceId.toString() : null; } public void setIconUrl(String iconUrl) { @@ -93,6 +94,8 @@ private Task finishModpackInstallingAsync(SettingsMap settings) { Charset charset = settings.get(LocalModpackPage.MODPACK_CHARSET); boolean isManuallyCreated = settings.getOrDefault(LocalModpackPage.MODPACK_MANUALLY_CREATED, false); + GameInstanceID instanceId = new GameInstanceID(name); + if (isManuallyCreated) { return ModpackHelper.getInstallManuallyCreatedModpackTask(selected, name, charset); } @@ -106,9 +109,9 @@ private Task finishModpackInstallingAsync(SettingsMap settings) { } try { if (serverModpackManifest != null) { - return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(name))); + return ModpackHelper.getUpdateTask(repository, serverModpackManifest, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(instanceId))); } else { - return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), name, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(name))); + return ModpackHelper.getUpdateTask(repository, selected, modpack.getEncoding(), instanceId, ModpackHelper.readModpackConfiguration(repository.getModpackConfiguration(instanceId))); } } catch (UnsupportedModpackException | ManuallyCreatedModpackException e) { Controllers.dialog(i18n("modpack.unsupported"), i18n("message.error"), MessageType.ERROR); @@ -120,11 +123,11 @@ private Task finishModpackInstallingAsync(SettingsMap settings) { return null; } else { if (serverModpackManifest != null) { - return ModpackHelper.getInstallTask(repository, serverModpackManifest, name, modpack) - .thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name)); + return ModpackHelper.getInstallTask(repository, serverModpackManifest, instanceId, modpack) + .thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(instanceId)); } else { - return ModpackHelper.getInstallTask(repository, selected, name, modpack, iconUrl) - .thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name)); + return ModpackHelper.getInstallTask(repository, selected, instanceId, modpack, iconUrl) + .thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(instanceId)); } } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java index 3c5443c3492..0baf1041d2f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackPage.java @@ -61,7 +61,7 @@ protected ModpackPage(WizardController controller) { { var archiveNamePane = new LinePane(); { - archiveNamePane.setTitle(i18n("version.name")); + archiveNamePane.setTitle(i18n("instance.name")); txtModpackName = new JFXTextField(); txtModpackName.setPrefWidth(300); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java index dc19d5ae391..a014be7803a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/ModpackSelectionPage.java @@ -27,6 +27,7 @@ import javafx.scene.layout.VBox; import javafx.scene.shape.SVGPath; import javafx.stage.FileChooser; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.modpack.server.ServerModpackManifest; import org.jackhuang.hmcl.task.FileDownloadTask; @@ -166,7 +167,7 @@ private void onChooseRemoteFile() { public void onChooseRepository() { String modPackName = controller.getSettings().get(MODPACK_NAME); - DownloadPage downloadPage = new DownloadPage(modPackName); + DownloadPage downloadPage = new DownloadPage(new GameInstanceID(modPackName)); downloadPage.showModpackDownloads(); Controllers.navigate(downloadPage); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java index 3da22a67ac5..bbda5ce46bd 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/RemoteModpackPage.java @@ -66,8 +66,8 @@ public RemoteModpackPage(WizardController controller) { txtModpackName.setText(manifest.getName().trim()); txtModpackName.getValidators().addAll( new RequiredValidator(), - new Validator(i18n("install.new_game.already_exists"), str -> !repository.versionIdConflicts(str)), - new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId)); + new Validator(i18n("install.new_game.already_exists"), str -> !repository.instanceIdConflicts(str)), + new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId)); } btnDescription.setVisible(StringUtils.isNotBlank(manifest.getDescription())); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java index 2a51d4d91db..00d6c064651 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/UpdateInstallerWizardProvider.java @@ -21,8 +21,8 @@ import org.jackhuang.hmcl.download.*; import org.jackhuang.hmcl.download.game.GameAssetIndexDownloadTask; import org.jackhuang.hmcl.download.game.LibraryDownloadException; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.task.DownloadException; import org.jackhuang.hmcl.task.Task; @@ -49,15 +49,15 @@ public final class UpdateInstallerWizardProvider implements WizardProvider { private final HMCLGameRepository repository; private final DefaultDependencyManager dependencyManager; private final String gameVersion; - private final Version version; + private final GameInstanceManifest manifest; private final String libraryId; private final String oldLibraryVersion; private final DownloadProvider downloadProvider; - public UpdateInstallerWizardProvider(@NotNull HMCLGameRepository repository, @NotNull String gameVersion, @NotNull Version version, @NotNull String libraryId, @Nullable String oldLibraryVersion) { + public UpdateInstallerWizardProvider(@NotNull HMCLGameRepository repository, @NotNull String gameVersion, @NotNull GameInstanceManifest manifest, @NotNull String libraryId, @Nullable String oldLibraryVersion) { this.repository = repository; this.gameVersion = gameVersion; - this.version = version; + this.manifest = manifest; this.libraryId = libraryId; this.oldLibraryVersion = oldLibraryVersion; this.downloadProvider = DownloadProviders.getDownloadProvider(); @@ -76,7 +76,7 @@ public Object finish(SettingsMap settings) { // We remove library but not save it, // so if installation failed will not break down current version. - Task ret = Task.supplyAsync(() -> version); + Task ret = Task.supplyAsync(() -> manifest); var hints = new ArrayList(); for (Object value : settings.asStringMap().values()) { if (value instanceof RemoteVersion remoteVersion) { @@ -91,7 +91,7 @@ public Object finish(SettingsMap settings) { } } - return ret.thenComposeAsync(repository::saveAsync).thenComposeAsync(repository.refreshVersionsAsync()).withStagesHints(hints); + return ret.thenComposeAsync(repository::saveAsync).thenComposeAsync(repository.refreshAsync()).withStagesHints(hints); } @Override @@ -103,7 +103,7 @@ public Node createPage(WizardController controller, int step, SettingsMap settin controller.onFinish(); } else if ("game".equals(libraryId)) { String newGameVersion = ((RemoteVersion) settings.get(libraryId)).getSelfVersion(); - controller.onNext(new AdditionalInstallersPage(newGameVersion, version, controller, repository, downloadProvider)); + controller.onNext(new AdditionalInstallersPage(newGameVersion, manifest, 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); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java index b0289431c94..e281d034f85 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VanillaInstallWizardProvider.java @@ -22,6 +22,7 @@ import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.task.Schedulers; @@ -50,10 +51,10 @@ public void start(SettingsMap settings) { } private Task finishVersionDownloadingAsync(SettingsMap settings) { - GameBuilder builder = dependencyManager.gameBuilder(); + GameBuilder builder = dependencyManager.newGameBuilder(); String name = (String) settings.get("name"); - builder.name(name); + builder.name(new GameInstanceID(name)); builder.gameVersion(((RemoteVersion) settings.get("game")).getGameVersion()); settings.asStringMap().forEach((key, value) -> { @@ -62,10 +63,10 @@ private Task finishVersionDownloadingAsync(SettingsMap settings) { }); return builder.buildAsync().whenComplete(any -> { - repository.refreshVersions(); - repository.applyDefaultIsolationSetting(name); + repository.refresh(); + repository.applyDefaultIsolationSetting(new GameInstanceID(name)); }) - .thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(name)); + .thenRunAsync(Schedulers.javafx(), () -> repository.setSelectedInstance(new GameInstanceID(name))); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java index b50448370c2..193d2955b45 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/download/VersionsPage.java @@ -45,7 +45,7 @@ import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion; import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion; import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion; -import org.jackhuang.hmcl.setting.VersionIconType; +import org.jackhuang.hmcl.setting.GameInstanceIconType; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.FXUtils; @@ -237,49 +237,49 @@ public void updateItem(RemoteVersion remoteVersion, boolean empty) { switch (versionType) { case RELEASE -> { - twoLineListItem.addTag(i18n("version.game.release")); - imageView.setImage(VersionIconType.GRASS.getIcon()); + twoLineListItem.addTag(i18n("instance.game.release")); + imageView.setImage(GameInstanceIconType.GRASS.getIcon()); } case SNAPSHOT, PENDING, UNOBFUSCATED -> { if (versionType == RemoteVersion.Type.SNAPSHOT && GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) { - twoLineListItem.addTag(i18n("version.game.april_fools")); - imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon()); + twoLineListItem.addTag(i18n("instance.game.april_fools")); + imageView.setImage(GameInstanceIconType.APRIL_FOOLS.getIcon()); } else { - twoLineListItem.addTag(i18n("version.game.snapshot")); - imageView.setImage(VersionIconType.COMMAND.getIcon()); + twoLineListItem.addTag(i18n("instance.game.snapshot")); + imageView.setImage(GameInstanceIconType.COMMAND.getIcon()); } } default -> { - twoLineListItem.addTag(i18n("version.game.old")); - imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon()); + twoLineListItem.addTag(i18n("instance.game.old")); + imageView.setImage(GameInstanceIconType.CRAFT_TABLE.getIcon()); } } switch (NativePatcher.checkSupportedStatus(gameVersion, Platform.SYSTEM_PLATFORM, OperatingSystem.SYSTEM_VERSION)) { - case UNTESTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.untested")); - case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("version.game.support_status.unsupported")); + case UNTESTED -> twoLineListItem.addTagWarning(i18n("instance.game.support_status.untested")); + case UNSUPPORTED -> twoLineListItem.addTagWarning(i18n("instance.game.support_status.unsupported")); } } else { - VersionIconType iconType; + GameInstanceIconType iconType; if (remoteVersion instanceof LiteLoaderRemoteVersion) - iconType = VersionIconType.CHICKEN; + iconType = GameInstanceIconType.CHICKEN; else if (remoteVersion instanceof OptiFineRemoteVersion) - iconType = VersionIconType.OPTIFINE; + iconType = GameInstanceIconType.OPTIFINE; else if (remoteVersion instanceof ForgeRemoteVersion) - iconType = VersionIconType.FORGE; + iconType = GameInstanceIconType.FORGE; else if (remoteVersion instanceof CleanroomRemoteVersion) - iconType = VersionIconType.CLEANROOM; + iconType = GameInstanceIconType.CLEANROOM; else if (remoteVersion instanceof NeoForgeRemoteVersion) - iconType = VersionIconType.NEO_FORGE; + iconType = GameInstanceIconType.NEO_FORGE; else if (remoteVersion instanceof LegacyFabricRemoteVersion || remoteVersion instanceof LegacyFabricAPIRemoteVersion) - iconType = VersionIconType.LEGACY_FABRIC; + iconType = GameInstanceIconType.LEGACY_FABRIC; else if (remoteVersion instanceof FabricRemoteVersion || remoteVersion instanceof FabricAPIRemoteVersion) - iconType = VersionIconType.FABRIC; + iconType = GameInstanceIconType.FABRIC; else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion) - iconType = VersionIconType.QUILT; + iconType = GameInstanceIconType.QUILT; else - iconType = VersionIconType.COMMAND; + iconType = GameInstanceIconType.COMMAND; imageView.setImage(iconType.getIcon()); String displayGameVersion = I18n.getDisplayVersion(GameVersionNumber.asGameVersion(remoteVersion.getGameVersion())); @@ -333,7 +333,7 @@ private static final class VersionsPageSkin extends SkinBase { { nameField = new JFXTextField(); - nameField.setPromptText(i18n("version.search.prompt")); + nameField.setPromptText(i18n("instance.search.prompt")); nameField.textProperty().addListener(o -> updateList()); if ("game".equals(control.libraryId)) { @@ -353,7 +353,7 @@ private static final class VersionsPageSkin extends SkinBase { ); categoryField.getSelectionModel().select(VersionTypeFilter.ALL); } - categoryField.setConverter(stringConverter(type -> i18n("version.game." + type.name().toLowerCase(Locale.ROOT)))); + categoryField.setConverter(stringConverter(type -> i18n("instance.game." + type.name().toLowerCase(Locale.ROOT)))); categoryField.getSelectionModel().selectedItemProperty().addListener(o -> updateList()); JFXButton refreshButton = FXUtils.newRaisedButton(i18n("button.refresh")); @@ -361,13 +361,13 @@ private static final class VersionsPageSkin extends SkinBase { if (control.versionList.hasType()) { searchPane.addRow(rowIndex++, - new Label(i18n("version.search")), nameField, - new Label(i18n("version.game.type")), categoryField, + new Label(i18n("instance.search")), nameField, + new Label(i18n("instance.game.type")), categoryField, refreshButton ); } else { searchPane.addRow(rowIndex++, - new Label(i18n("version.search")), nameField, + new Label(i18n("instance.search")), nameField, refreshButton ); } @@ -393,7 +393,7 @@ private static final class VersionsPageSkin extends SkinBase { RemoteVersionListCell dummyCell = new RemoteVersionListCell(control); dummyCell.twoLineListItem.setTitle("Dummy"); dummyCell.twoLineListItem.setSubtitle("Dummy"); - dummyCell.imageView.setImage(VersionIconType.GRASS.getIcon()); + dummyCell.imageView.setImage(GameInstanceIconType.GRASS.getIcon()); prepareNode(dummyCell.pane); list.setFixedCellSize(dummyCell.pane.prefHeight(-1)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java index cc667128769..28d08dadc22 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ExportWizardProvider.java @@ -19,6 +19,7 @@ import javafx.scene.Node; import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.modpack.ModAdviser; import org.jackhuang.hmcl.modpack.ModpackExportInfo; @@ -47,11 +48,11 @@ public final class ExportWizardProvider implements WizardProvider { private final HMCLGameRepository repository; - private final String version; + private final GameInstanceID instanceId; - public ExportWizardProvider(HMCLGameRepository repository, String version) { + public ExportWizardProvider(HMCLGameRepository repository, GameInstanceID instanceId) { this.repository = repository; - this.version = version; + this.instanceId = instanceId; } @Override @@ -164,7 +165,7 @@ private Task exportAsMcbbs(ModpackExportInfo exportInfo, Path modpackFile) { @Override public void execute() { - dependency = new McbbsModpackExportTask(repository, version, exportInfo, modpackFile); + dependency = new McbbsModpackExportTask(repository, instanceId, exportInfo, modpackFile); } @Override @@ -184,8 +185,8 @@ private Task exportAsMultiMC(ModpackExportInfo exportInfo, Path modpackFile) @Override public void execute() { - GameSettings.Effective setting = repository.getEffectiveGameSettings(version); - dependency = new MultiMCModpackExportTask(repository, version, exportInfo.getWhitelist(), + GameSettings.Effective setting = repository.getEffectiveGameSettings(instanceId); + dependency = new MultiMCModpackExportTask(repository, instanceId, exportInfo.getWhitelist(), new MultiMCInstanceConfiguration( "OneSix", exportInfo.getName() + "-" + exportInfo.getVersion(), @@ -232,7 +233,7 @@ private Task exportAsServer(ModpackExportInfo exportInfo, Path modpackFile) { @Override public void execute() { - dependency = new ServerModpackExportTask(repository, version, exportInfo, modpackFile); + dependency = new ServerModpackExportTask(repository, instanceId, exportInfo, modpackFile); } @Override @@ -254,7 +255,7 @@ private Task exportAsModrinth(ModpackExportInfo exportInfo, Path modpackFile) public void execute() { dependency = new ModrinthModpackExportTask( repository, - version, + instanceId, exportInfo, modpackFile ); @@ -271,8 +272,8 @@ public Collection> getDependencies() { public Node createPage(WizardController controller, int step, SettingsMap settings) { return switch (step) { case 0 -> new ModpackTypeSelectionPage(controller); - case 1 -> new ModpackInfoPage(controller, repository, version); - case 2 -> new ModpackFileSelectionPage(controller, repository, version, ModAdviser::suggestMod); + case 1 -> new ModpackInfoPage(controller, repository, instanceId); + case 2 -> new ModpackFileSelectionPage(controller, repository, instanceId, ModAdviser::suggestMod); default -> throw new IllegalArgumentException("step"); }; } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackFileSelectionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackFileSelectionPage.java index 3feb91781b4..88c437b27ac 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackFileSelectionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackFileSelectionPage.java @@ -27,6 +27,7 @@ import javafx.scene.control.TreeItem; import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.modpack.ModAdviser; import org.jackhuang.hmcl.ui.FXUtils; @@ -56,17 +57,17 @@ */ public final class ModpackFileSelectionPage extends BorderPane implements WizardPage { private final WizardController controller; - private final String version; + private final GameInstanceID instanceId; private final ModAdviser adviser; private final ModpackFileTreeItem rootNode; - public ModpackFileSelectionPage(WizardController controller, HMCLGameRepository repository, String version, ModAdviser adviser) { + public ModpackFileSelectionPage(WizardController controller, HMCLGameRepository repository, GameInstanceID instanceId, ModAdviser adviser) { this.controller = controller; - this.version = version; + this.instanceId = instanceId; this.adviser = adviser; JFXTreeView treeView = new JFXTreeView<>(); - rootNode = getTreeItem(repository.getRunDirectory(version), "minecraft", 0); + rootNode = getTreeItem(repository.getRunDirectory(instanceId), "minecraft", 0); treeView.setRoot(rootNode); treeView.setSelectionModel(new NoneMultipleSelectionModel<>()); onEscPressed(treeView, () -> controller.onPrev(true)); @@ -107,12 +108,12 @@ private ModpackFileTreeItem getTreeItem(Path file, String basePath, int level) { } if (fileName.startsWith("._")) // macOS system file state = ModAdviser.ModSuggestion.HIDDEN; - if (FileUtils.getNameWithoutExtension(file).equals(version)) + if (FileUtils.getNameWithoutExtension(file).equals(instanceId.toString())) state = ModAdviser.ModSuggestion.HIDDEN; } if (isDirectory) { - if (fileName.equals(version + "-natives")) { // Ignore -natives + if (fileName.equals(instanceId + "-natives")) { // Ignore -natives state = ModAdviser.ModSuggestion.HIDDEN; } if (level == 1 && fileName.startsWith("natives-")) { // Ignore natives-os-arch @@ -123,7 +124,7 @@ private ModpackFileTreeItem getTreeItem(Path file, String basePath, int level) { return null; } - ModpackFileTreeItem node = new ModpackFileTreeItem(level == 0 ? version : StringUtils.substringAfterLast(basePath, '/'), basePath); + ModpackFileTreeItem node = new ModpackFileTreeItem(level == 0 ? instanceId.toString() : StringUtils.substringAfterLast(basePath, '/'), basePath); if (state == ModAdviser.ModSuggestion.SUGGESTED) node.setSelected(true); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java index 402f0fb50d3..421518be86c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/export/ModpackInfoPage.java @@ -35,6 +35,7 @@ import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorServer; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.modpack.ModpackExportInfo; import org.jackhuang.hmcl.modpack.mcbbs.McbbsModpackManifest; @@ -65,9 +66,9 @@ public final class ModpackInfoPage extends Control implements WizardPage { private final WizardController controller; - private final HMCLGameRepository gameRepository; + private final HMCLGameRepository repository; private final ModpackExportInfo.Options options; - private final String versionName; + private final GameInstanceID instanceId; private final boolean canIncludeLauncher; private final ModpackExportInfo exportInfo = new ModpackExportInfo(); @@ -88,19 +89,19 @@ public final class ModpackInfoPage extends Control implements WizardPage { private final SimpleBooleanProperty noCreateRemoteFiles = new SimpleBooleanProperty(); private final SimpleBooleanProperty skipCurseForgeRemoteFiles = new SimpleBooleanProperty(); - public ModpackInfoPage(WizardController controller, HMCLGameRepository gameRepository, String version) { + public ModpackInfoPage(WizardController controller, HMCLGameRepository repository, GameInstanceID instanceId) { this.controller = controller; - this.gameRepository = gameRepository; + this.repository = repository; this.options = controller.getSettings().get(MODPACK_INFO_OPTION); - this.versionName = version; + this.instanceId = instanceId; if (this.options == null) throw new IllegalArgumentException("Settings.MODPACK_INFO_OPTION is required"); - name.set(version); + name.set(instanceId.toString()); author.set(Optional.ofNullable(Accounts.getSelectedAccount()).map(Account::getProfileName).orElse("")); - GameSettings.Effective versionSetting = gameRepository.getEffectiveGameSettings(versionName); + GameSettings.Effective versionSetting = repository.getEffectiveGameSettings(this.instanceId); minMemory.set(Optional.ofNullable(versionSetting.get(GameSettings::minMemoryProperty)).orElse(0)); launchArguments.set(versionSetting.get(GameSettings::gameArgumentsProperty)); javaArguments.set(versionSetting.get(GameSettings::jvmOptionsProperty)); @@ -213,7 +214,7 @@ public ModpackInfoPageSkin(ModpackInfoPage skinnable) { var instanceNamePane = new LineTextPane(); { instanceNamePane.setTitle(i18n("modpack.wizard.step.initialization.exported_version")); - instanceNamePane.setText(skinnable.versionName); + instanceNamePane.setText(skinnable.instanceId.toString()); list.getContent().add(instanceNamePane); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java index 54fef4e3b21..26a2fe263da 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/game/GameSettingsPage.java @@ -50,8 +50,8 @@ import org.jackhuang.hmcl.ui.*; 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.instances.GameInstanceIconDialog; +import org.jackhuang.hmcl.ui.instances.GameInstancePage; import org.jackhuang.hmcl.util.Holder; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.ServerAddress; @@ -78,7 +78,7 @@ /// @author Glavo @NotNullByDefault public final class GameSettingsPage extends StackPane - implements DecoratorPage, VersionPage.GameInstanceLoadable, PageAware { + implements DecoratorPage, GameInstancePage.GameInstanceLoadable, PageAware { private static final Object INHERIT_BUTTON_TOOLTIP_KEY = new Object(); private static final PseudoClass PSEUDO_OVERRIDDEN = PseudoClass.getPseudoClass("overridden"); @@ -97,7 +97,7 @@ public final class GameSettingsPage extends StackPane private @Nullable HMCLGameRepository repository; /// The current instance ID. - private @Nullable String instanceId; + private @Nullable GameInstanceID instanceId; /// The current setting. private final ObjectProperty<@Nullable S> currentSetting = new SimpleObjectProperty<>(this, "setting"); @@ -1799,7 +1799,7 @@ private String getCurrentInstanceVersionRoot() { return ""; } - return repository.getVersionRoot(instanceId).toString(); + return repository.getInstanceRoot(instanceId).toString(); } /// Keeps a listener attached to the current instance's parent preset property. @@ -2440,7 +2440,7 @@ public ReadOnlyObjectProperty stateProperty() { @SuppressWarnings("unchecked") @Override - public void loadInstance(HMCLGameRepository repository, @Nullable String instanceId) { + public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) { this.gameDirectory = repository.getGameDirectory(); this.repository = repository; this.instanceId = instanceId; @@ -2467,6 +2467,11 @@ public void loadInstance(HMCLGameRepository repository, @Nullable String instanc } } + /// Returns the loaded instance ID, or `null` when this page edits preset settings. + private @Nullable GameInstanceID getLoadedInstanceId() { + return instanceId == null ? null : instanceId; + } + /// Updates the page read-only state used when settings cannot be saved safely. /// /// @param readOnly whether the current settings should be displayed read-only @@ -2506,12 +2511,13 @@ private void setSettingsReadOnly(boolean readOnly, String message, @Nullable Run /// Backs up and overwrites the current instance's `instance-game-settings.json`. private void forceOverwriteInstanceGameSettings() { - if (repository == null || instanceId == null) { + @Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId(); + if (repository == null || loadedInstanceId == null) { return; } Controllers.confirmBackupAndOverwrite(i18n("settings.game.instance_settings.unsupported"), () -> { - repository.forceOverwriteInstanceGameSettings(instanceId); + repository.forceOverwriteInstanceGameSettings(loadedInstanceId); setSettingsReadOnly(false, ""); }); } @@ -2525,10 +2531,11 @@ private void forceOverwriteGameSettings() { } private void loadIcon() { - if (repository == null || instanceId == null) + @Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId(); + if (repository == null || loadedInstanceId == null) return; - iconPickerItem.setImage(repository.getVersionIconImage(instanceId)); + iconPickerItem.setImage(repository.getInstanceIconImage(loadedInstanceId)); } /// Refreshes Java selection controls and keeps inherited parent Java properties observed. @@ -2597,7 +2604,8 @@ private void initJavaSubtitle() { initializeSelectedJava(); JavaVersionType javaVersionType = setting.javaTypeProperty().getValue(); - GameSettings.Effective effectiveSetting = this.instanceId != null && repository != null ? repository.getEffectiveGameSettings(this.instanceId) : null; + @Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId(); + GameSettings.Effective effectiveSetting = loadedInstanceId != null && repository != null ? repository.getEffectiveGameSettings(loadedInstanceId) : null; JavaVersionType effectiveJavaVersionType = effectiveSetting != null ? effectiveSetting.getInheritable(GameSettings::javaTypeProperty) : javaVersionType; boolean autoSelected = effectiveJavaVersionType == JavaVersionType.AUTO || effectiveJavaVersionType == JavaVersionType.VERSION; @@ -2614,17 +2622,17 @@ private void initJavaSubtitle() { if (JavaManager.isInitialized()) { GameVersionNumber gameVersionNumber = this.currentGameVersionNumber.get(); - Version version; + GameInstanceManifest manifest; if (this.instanceId == null) { - version = null; + manifest = null; } else { - version = repository != null ? repository.getResolvedVersion(this.instanceId) : null; + manifest = repository != null && loadedInstanceId != null ? repository.getResolvedInstanceManifest(loadedInstanceId).launchManifest() : null; } try { JavaRuntime java = effectiveSetting != null - ? effectiveSetting.getJava(gameVersionNumber, version) - : resolveEffectiveSetting(setting).getJava(gameVersionNumber, version); + ? effectiveSetting.getJava(gameVersionNumber, manifest) + : resolveEffectiveSetting(setting).getJava(gameVersionNumber, manifest); if (java != null) { javaSublist.setDescription(java.getBinary().toString()); } else { @@ -2642,17 +2650,18 @@ private void onExploreIcon() { if (repository == null || instanceId == null) return; - Controllers.dialog(new VersionIconDialog(repository, instanceId, this::loadIcon)); + Controllers.dialog(new GameInstanceIconDialog(repository, instanceId, this::loadIcon)); } private void onDeleteIcon() { - if (repository == null || instanceId == null) + @Nullable GameInstanceID loadedInstanceId = getLoadedInstanceId(); + if (repository == null || loadedInstanceId == null) return; - repository.deleteIconFile(instanceId); - GameSettings.Instance localGameSettings = repository.getInstanceGameSettingsOrCreate(instanceId); + repository.deleteIconFile(loadedInstanceId); + GameSettings.Instance localGameSettings = repository.getInstanceGameSettingsOrCreate(loadedInstanceId); if (localGameSettings != null) { - localGameSettings.iconProperty().setValue(VersionIconType.DEFAULT); + localGameSettings.iconProperty().setValue(GameInstanceIconType.DEFAULT); } loadIcon(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonCheckUpdatesTask.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonCheckUpdatesTask.java index 47597d34a63..531bbaa94fb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonCheckUpdatesTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonCheckUpdatesTask.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.addon.LocalAddonFile; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java index b9c251715f6..1f9fbb7f022 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/AddonUpdatesPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/AddonUpdatesPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java index 76d130b17a0..e2218bb99bb 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import javafx.beans.property.BooleanProperty; import javafx.collections.ObservableList; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java index 40fc80daf59..bdbf7e16302 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DataPackListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DataPackListPageSkin.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java similarity index 92% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java index bd1f09f6973..f82992d90e4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadListPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXComboBox; @@ -38,8 +38,9 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.*; import org.jackhuang.hmcl.download.DownloadProvider; +import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameInstanceManifest; 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; @@ -67,17 +68,17 @@ 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.GameInstanceLoadable { +public class DownloadListPage extends Control implements DecoratorPage, GameInstancePage.GameInstanceLoadable { protected final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final BooleanProperty loading = new SimpleBooleanProperty(false); private final BooleanProperty failed = new SimpleBooleanProperty(false); - private final boolean versionSelection; + private final boolean instanceSelection; private final ObjectProperty instanceReference = new SimpleObjectProperty<>(); private final IntegerProperty pageOffset = new SimpleIntegerProperty(0); private final IntegerProperty pageCount = new SimpleIntegerProperty(-1); private final ListProperty items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList()); - private final ObservableList versions = FXCollections.observableArrayList(); - private final StringProperty selectedVersion = new SimpleStringProperty(); + private final ObservableList instances = FXCollections.observableArrayList(); + private final ObjectProperty selectedInstance = new SimpleObjectProperty<>(); private final DownloadPage.DownloadCallback callback; private boolean searchInitialized = false; protected final BooleanProperty supportChinese = new SimpleBooleanProperty(); @@ -95,10 +96,10 @@ public DownloadListPage(RemoteAddonRepository repository) { this(repository, null, false); } - public DownloadListPage(RemoteAddonRepository repository, DownloadPage.DownloadCallback callback, boolean versionSelection) { + public DownloadListPage(RemoteAddonRepository repository, DownloadPage.DownloadCallback callback, boolean instanceSelection) { this.repository = repository; this.callback = callback; - this.versionSelection = versionSelection; + this.instanceSelection = instanceSelection; this.downloadProvider = DownloadProviders.getDownloadProvider(); } @@ -111,7 +112,7 @@ public ObservableList getActions() { } @Override - public void loadInstance(HMCLGameRepository repository, @Nullable String instanceId) { + public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) { this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, instanceId)); setLoading(false); @@ -122,11 +123,11 @@ public void loadInstance(HMCLGameRepository repository, @Nullable String instanc search("", null, 0, "", RemoteAddonRepository.SortType.POPULARITY); } - if (versionSelection) { - versions.setAll(repository.getDisplayVersions() - .map(Version::getId) - .collect(Collectors.toList())); - selectedVersion.set(repository.getSelectedInstance()); + if (instanceSelection) { + instances.setAll(repository.getDisplayInstanceManifests() + .map(GameInstanceManifest::id) + .toList()); + selectedInstance.set(repository.getSelectedInstance()); } } @@ -154,8 +155,8 @@ public void setLoading(boolean loading) { this.loading.set(loading); } - public void selectVersion(String versionID) { - FXUtils.runInFX(() -> selectedVersion.set(versionID)); + public void selectInstance(GameInstanceID instanceId) { + FXUtils.runInFX(() -> selectedInstance.set(instanceId)); } private void search(String userGameVersion, RemoteAddonRepository.Category category, int pageOffset, String searchFilter, RemoteAddonRepository.SortType sort) { @@ -166,12 +167,10 @@ private void search(String userGameVersion, RemoteAddonRepository.Category categ int currentSearchID = searchID = searchID + 1; Task.supplyAsync(() -> { HMCLGameRepository.InstanceReference instanceReference = this.instanceReference.get(); - if (StringUtils.isBlank(instanceReference.instanceId())) { + if (instanceReference.instanceId() == null) { return userGameVersion; } else { - return StringUtils.isNotBlank(instanceReference.instanceId()) - ? instanceReference.repository().getGameVersion(instanceReference.instanceId()).orElse("") - : ""; + return instanceReference.repository().getGameVersion(instanceReference.instanceId()).orElse(""); } }).thenApplyAsync( gameVersion -> repository.search(downloadProvider, gameVersion, category, pageOffset, 50, searchFilter, sort, RemoteAddonRepository.SortOrder.DESC) @@ -219,8 +218,9 @@ protected String getLocalizedOfficialPage() { } protected HMCLGameRepository.InstanceReference getInstanceReference() { - if (versionSelection) { - return new HMCLGameRepository.InstanceReference(instanceReference.get().repository(), selectedVersion.get()); + if (instanceSelection) { + @Nullable GameInstanceID instanceId = selectedInstance.get(); + return new HMCLGameRepository.InstanceReference(instanceReference.get().repository(), instanceId); } else { return instanceReference.get(); } @@ -273,18 +273,18 @@ protected ModDownloadListPageSkin(DownloadListPage control) { { int rowIndex = 0; - if (control.versionSelection || !control.downloadSources.isEmpty()) { + if (control.instanceSelection || !control.downloadSources.isEmpty()) { searchPane.addRow(rowIndex); int columns = 0; Node lastNode = null; - if (control.versionSelection) { - JFXComboBox versionsComboBox = new JFXComboBox<>(); - versionsComboBox.setMaxWidth(Double.MAX_VALUE); - Bindings.bindContent(versionsComboBox.getItems(), control.versions); - selectedItemPropertyFor(versionsComboBox).bindBidirectional(control.selectedVersion); - - searchPane.add(new Label(i18n("version")), columns++, rowIndex); - searchPane.add(lastNode = versionsComboBox, columns++, rowIndex); + if (control.instanceSelection) { + JFXComboBox instancesComboBox = new JFXComboBox<>(); + instancesComboBox.setMaxWidth(Double.MAX_VALUE); + Bindings.bindContent(instancesComboBox.getItems(), control.instances); + selectedItemPropertyFor(instancesComboBox).bindBidirectional(control.selectedInstance); + + searchPane.add(new Label(i18n("instance")), columns++, rowIndex); + searchPane.add(lastNode = instancesComboBox, columns++, rowIndex); } if (control.downloadSources.getSize() > 1) { @@ -329,7 +329,7 @@ protected ModDownloadListPageSkin(DownloadListPage control) { FXUtils.installFastTooltip(gameVersionField, i18n("search.enter")); FXUtils.onChangeAndOperate(getSkinnable().instanceReference, instanceReference -> { - if (StringUtils.isNotBlank(instanceReference.instanceId())) { + if (instanceReference.instanceId() != null) { GridPane.setColumnSpan(nameField, 3); } else { GridPane.setColumnSpan(nameField, 1); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java index fb06c3ca0fe..85325afba25 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/DownloadPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/DownloadPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; @@ -31,8 +31,9 @@ import javafx.stage.FileChooser; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.HMCLGameRepository; -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; @@ -271,13 +272,13 @@ protected DownloadPageSkin(DownloadPage control) { 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); + GameInstanceManifest.Resolved resolvedManifest = repository.getResolvedInstanceManifest(control.instanceReference.instanceId()); + String gameVersion = repository.getGameVersion(resolvedManifest.unresolved()).orElse(null); if (gameVersion != null && control.versions.containsKey(gameVersion)) { List modVersions = control.versions.get(gameVersion); if (modVersions != null && !modVersions.isEmpty()) { - Set targetLoaders = LibraryAnalyzer.analyze(game, gameVersion).getModLoaders(); + Set targetLoaders = LibraryAnalyzer.analyze(resolvedManifest, gameVersion).getModLoaders(); resolve: for (RemoteAddon.Version modVersion : modVersions) { @@ -615,6 +616,6 @@ private void loadDependencies(RemoteAddon.Version version, DownloadPage selfPage @FunctionalInterface public interface DownloadCallback { - void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable String version, RemoteAddon addon, RemoteAddon.Version file); + void download(DownloadProvider downloadProvider, HMCLGameRepository repository, @Nullable GameInstanceID instanceId, RemoteAddon addon, RemoteAddon.Version file); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameAdvancedListItem.java similarity index 67% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameAdvancedListItem.java index d280204518f..19bfb72f77d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameAdvancedListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameAdvancedListItem.java @@ -15,13 +15,14 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import javafx.geometry.Pos; import org.jackhuang.hmcl.event.Event; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.GameDirectoryManager; -import org.jackhuang.hmcl.setting.VersionIconType; +import org.jackhuang.hmcl.setting.GameInstanceIconType; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.WeakListenerHolder; import org.jackhuang.hmcl.ui.construct.AdvancedListItem; @@ -36,7 +37,7 @@ public class GameAdvancedListItem extends AdvancedListItem { private final WeakListenerHolder holder = new WeakListenerHolder(); private HMCLGameRepository repository; @SuppressWarnings("unused") - private Consumer onVersionIconChangedListener; + private Consumer onInstanceIconChangedListener; public GameAdvancedListItem() { this.imageContainer = new ImageContainer(LEFT_GRAPHIC_SIZE); @@ -44,26 +45,29 @@ public GameAdvancedListItem() { AdvancedListItem.setAlignment(imageContainer, Pos.CENTER); setLeftGraphic(imageContainer); - holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), this::loadVersion)); + holder.add(FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), this::loadInstance)); } - private void loadVersion(String version) { + private void loadInstance(GameInstanceID instanceId) { if (GameDirectoryManager.getSelectedRepository() != repository) { repository = GameDirectoryManager.getSelectedRepository(); if (repository != null) { - onVersionIconChangedListener = repository.onVersionIconChanged.registerWeak(event -> { - this.loadVersion(repository.getSelectedInstance()); + onInstanceIconChangedListener = repository.onInstanceIconChanged.registerWeak(event -> { + this.loadInstance(repository.getSelectedInstance()); }); } } - if (version != null && repository != null && repository.hasVersion(version)) { - setTitle(i18n("version.manage.manage")); - setSubtitle(version); - imageContainer.setImage(repository.getVersionIconImage(version)); - } else { - setTitle(i18n("version.empty")); - setSubtitle(i18n("version.empty.add")); - imageContainer.setImage(VersionIconType.DEFAULT.getIcon()); + if (instanceId != null && repository != null) { + if (repository.hasInstance(instanceId)) { + setTitle(i18n("instance.manage.manage")); + setSubtitle(instanceId.toString()); + imageContainer.setImage(repository.getInstanceIconImage(instanceId)); + return; + } } + + setTitle(i18n("instance.empty")); + setSubtitle(i18n("instance.empty.add")); + imageContainer.setImage(GameInstanceIconType.DEFAULT.getIcon()); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstanceIconDialog.java similarity index 69% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstanceIconDialog.java index 99378fa6869..fcd902f3951 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionIconDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstanceIconDialog.java @@ -15,16 +15,17 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import javafx.scene.Node; 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.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.GameSettings; -import org.jackhuang.hmcl.setting.VersionIconType; +import org.jackhuang.hmcl.setting.GameInstanceIconType; import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; @@ -38,17 +39,17 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class VersionIconDialog extends DialogPane { +public class GameInstanceIconDialog extends DialogPane { private final HMCLGameRepository repository; - private final String versionId; + private final GameInstanceID instanceId; private final Runnable onFinish; private final GameSettings.Instance setting; - public VersionIconDialog(HMCLGameRepository repository, String versionId, Runnable onFinish) { + public GameInstanceIconDialog(HMCLGameRepository repository, GameInstanceID instanceId, Runnable onFinish) { this.repository = repository; - this.versionId = versionId; + this.instanceId = instanceId; this.onFinish = onFinish; - this.setting = repository.getInstanceGameSettingsOrCreate(versionId); + this.setting = repository.getInstanceGameSettingsOrCreate(this.instanceId); setTitle(i18n("settings.icon")); FlowPane pane = new FlowPane(); @@ -56,20 +57,20 @@ public VersionIconDialog(HMCLGameRepository repository, String versionId, Runnab pane.getChildren().setAll( createCustomIcon(), - createIcon(VersionIconType.GRASS), - createIcon(VersionIconType.CHEST), - createIcon(VersionIconType.CHICKEN), - createIcon(VersionIconType.COMMAND), - createIcon(VersionIconType.APRIL_FOOLS), - createIcon(VersionIconType.OPTIFINE), - createIcon(VersionIconType.CRAFT_TABLE), - createIcon(VersionIconType.FABRIC), - createIcon(VersionIconType.LEGACY_FABRIC), - createIcon(VersionIconType.FORGE), - createIcon(VersionIconType.CLEANROOM), - createIcon(VersionIconType.NEO_FORGE), - createIcon(VersionIconType.FURNACE), - createIcon(VersionIconType.QUILT) + createIcon(GameInstanceIconType.GRASS), + createIcon(GameInstanceIconType.CHEST), + createIcon(GameInstanceIconType.CHICKEN), + createIcon(GameInstanceIconType.COMMAND), + createIcon(GameInstanceIconType.APRIL_FOOLS), + createIcon(GameInstanceIconType.OPTIFINE), + createIcon(GameInstanceIconType.CRAFT_TABLE), + createIcon(GameInstanceIconType.FABRIC), + createIcon(GameInstanceIconType.LEGACY_FABRIC), + createIcon(GameInstanceIconType.FORGE), + createIcon(GameInstanceIconType.CLEANROOM), + createIcon(GameInstanceIconType.NEO_FORGE), + createIcon(GameInstanceIconType.FURNACE), + createIcon(GameInstanceIconType.QUILT) ); } @@ -79,10 +80,10 @@ private void exploreIcon() { Path selectedFile = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage())); if (selectedFile != null) { try { - repository.setVersionIconFile(versionId, selectedFile); + repository.setInstanceIconFile(instanceId, selectedFile); if (setting != null) { - setting.iconProperty().setValue(VersionIconType.DEFAULT); + setting.iconProperty().setValue(GameInstanceIconType.DEFAULT); } onAccept(); @@ -102,7 +103,7 @@ private Node createCustomIcon() { return container; } - private Node createIcon(VersionIconType type) { + private Node createIcon(GameInstanceIconType type) { ImageView imageView = new ImageView(type.getIcon()); imageView.setMouseTransparent(true); RipplerContainer container = new RipplerContainer(imageView); @@ -119,7 +120,7 @@ private Node createIcon(VersionIconType type) { @Override protected void onAccept() { - repository.onVersionIconChanged.fireEvent(new Event(this)); + repository.onInstanceIconChanged.fireEvent(new Event(this)); onFinish.run(); super.onAccept(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java similarity index 68% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java index 860c7bf6c78..b27d2eff241 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/VersionPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameInstancePage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXPopup; import javafx.application.Platform; @@ -28,7 +28,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.event.RefreshedGameInstancesEvent; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.GameSettings; import org.jackhuang.hmcl.task.Schedulers; @@ -53,21 +54,21 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class VersionPage extends DecoratorAnimatedPage implements DecoratorPage { +public class GameInstancePage extends DecoratorAnimatedPage implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); private final TabHeader tab; - private final TabHeader.Tab> versionSettingsTab = new TabHeader.Tab<>("versionSettingsTab"); + private final TabHeader.Tab> gameSettingsTab = new TabHeader.Tab<>("versionSettingsTab"); private final TabHeader.Tab installerListTab = new TabHeader.Tab<>("installerListTab"); private final TabHeader.Tab modListTab = new TabHeader.Tab<>("modListTab"); private final TabHeader.Tab worldListTab = new TabHeader.Tab<>("worldList"); private final TabHeader.Tab schematicsTab = new TabHeader.Tab<>("schematicsTab"); private final TabHeader.Tab resourcePackTab = new TabHeader.Tab<>("resourcePackTab"); private final TransitionPane transitionPane = new TransitionPane(); - private final BooleanProperty currentVersionUpgradable = new SimpleBooleanProperty(); + private final BooleanProperty currentInstanceUpgradable = new SimpleBooleanProperty(); private final ObjectProperty instanceReference = new SimpleObjectProperty<>(); private final WeakListenerHolder listenerHolder = new WeakListenerHolder(); - private String preferredVersionName = null; + private GameInstanceID preferredInstanceId = null; public static class WorkingDirChangedEvent extends Event { public static final EventType EVENT_TYPE = new EventType<>(Event.ANY, "WORKING_DIR_CHANGED"); @@ -77,44 +78,45 @@ public WorkingDirChangedEvent() { } } - public VersionPage() { - versionSettingsTab.setNodeSupplier(loadVersionFor(() -> new GameSettingsPage<>(GameSettings.Instance.class))); - installerListTab.setNodeSupplier(loadVersionFor(InstallerListPage::new)); - modListTab.setNodeSupplier(loadVersionFor(ModListPage::new)); - resourcePackTab.setNodeSupplier(loadVersionFor(ResourcePackListPage::new)); - worldListTab.setNodeSupplier(loadVersionFor(WorldListPage::new)); - schematicsTab.setNodeSupplier(loadVersionFor(SchematicsPage::new)); + public GameInstancePage() { + gameSettingsTab.setNodeSupplier(loadInstanceFor(() -> new GameSettingsPage<>(GameSettings.Instance.class))); + installerListTab.setNodeSupplier(loadInstanceFor(InstallerListPage::new)); + modListTab.setNodeSupplier(loadInstanceFor(ModListPage::new)); + resourcePackTab.setNodeSupplier(loadInstanceFor(ResourcePackListPage::new)); + worldListTab.setNodeSupplier(loadInstanceFor(WorldListPage::new)); + schematicsTab.setNodeSupplier(loadInstanceFor(SchematicsPage::new)); - tab = new TabHeader(transitionPane, versionSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab); - tab.select(versionSettingsTab); + tab = new TabHeader(transitionPane, gameSettingsTab, installerListTab, modListTab, resourcePackTab, worldListTab, schematicsTab); + tab.select(gameSettingsTab); addEventHandler(Navigator.NavigationEvent.NAVIGATED, this::onNavigated); addEventHandler(WorkingDirChangedEvent.EVENT_TYPE, event -> { if (this.instanceReference.get() != null) { if (installerListTab.isInitialized()) - installerListTab.getNode().loadInstance(getRepository(), getVersion()); + installerListTab.getNode().loadInstance(getRepository(), getInstanceId()); if (modListTab.isInitialized()) - modListTab.getNode().loadInstance(getRepository(), getVersion()); + modListTab.getNode().loadInstance(getRepository(), getInstanceId()); if (resourcePackTab.isInitialized()) - resourcePackTab.getNode().loadInstance(getRepository(), getVersion()); + resourcePackTab.getNode().loadInstance(getRepository(), getInstanceId()); if (worldListTab.isInitialized()) - worldListTab.getNode().loadInstance(getRepository(), getVersion()); + worldListTab.getNode().loadInstance(getRepository(), getInstanceId()); if (schematicsTab.isInitialized()) - schematicsTab.getNode().loadInstance(getRepository(), getVersion()); + schematicsTab.getNode().loadInstance(getRepository(), getInstanceId()); } }); - listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class).registerWeak(event -> checkSelectedVersion(), EventPriority.HIGHEST)); + listenerHolder.add(EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class).registerWeak(event -> checkSelectedInstnace(), EventPriority.HIGHEST)); } - private void checkSelectedVersion() { + private void checkSelectedInstnace() { runInFX(() -> { if (this.instanceReference.get() == null) return; HMCLGameRepository repository = this.instanceReference.get().repository(); - if (!repository.hasVersion(this.instanceReference.get().instanceId())) { - if (preferredVersionName != null) { - loadVersion(preferredVersionName, repository); + @Nullable GameInstanceID instanceId = this.instanceReference.get().instanceId(); + if (instanceId == null || !repository.hasInstance(instanceId)) { + if (preferredInstanceId != null) { + loadInstance(preferredInstanceId, repository); } else { fireEvent(new PageCloseEvent()); } @@ -122,12 +124,13 @@ private void checkSelectedVersion() { }); } - private Supplier loadVersionFor(Supplier nodeSupplier) { + private Supplier loadInstanceFor(Supplier nodeSupplier) { return () -> { T node = nodeSupplier.get(); if (instanceReference.get() != null) { - if (node instanceof VersionPage.GameInstanceLoadable) { - ((GameInstanceLoadable) node).loadInstance(instanceReference.get().repository(), instanceReference.get().instanceId()); + if (node instanceof GameInstancePage.GameInstanceLoadable loadable) { + @Nullable GameInstanceID instanceId = instanceReference.get().instanceId(); + loadable.loadInstance(instanceReference.get().repository(), instanceId); } } return node; @@ -135,38 +138,38 @@ private Supplier loadVersionFor(Supplier nodeSupplier) { } public void showInstanceSettings() { - tab.select(versionSettingsTab, false); + tab.select(gameSettingsTab, false); } - public void setVersion(String version, HMCLGameRepository repository) { - this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, version)); + public void setInstance(GameInstanceID instanceId, HMCLGameRepository repository) { + this.instanceReference.set(new HMCLGameRepository.InstanceReference(repository, instanceId)); } - public void loadVersion(String version, HMCLGameRepository repository) { + public void loadInstance(GameInstanceID instanceId, 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.instanceReference.get() != null && (!getRepository().isLoaded() || - !getRepository().hasVersion(version))) { + !getRepository().hasInstance(instanceId))) { Platform.runLater(() -> fireEvent(new PageCloseEvent())); return; } - setVersion(version, repository); - preferredVersionName = version; + setInstance(instanceId, repository); + preferredInstanceId = instanceId; - if (versionSettingsTab.isInitialized()) - versionSettingsTab.getNode().loadInstance(repository, version); + if (gameSettingsTab.isInitialized()) + gameSettingsTab.getNode().loadInstance(repository, instanceId); if (installerListTab.isInitialized()) - installerListTab.getNode().loadInstance(repository, version); + installerListTab.getNode().loadInstance(repository, instanceId); if (modListTab.isInitialized()) - modListTab.getNode().loadInstance(repository, version); + modListTab.getNode().loadInstance(repository, instanceId); if (resourcePackTab.isInitialized()) - resourcePackTab.getNode().loadInstance(repository, version); + resourcePackTab.getNode().loadInstance(repository, instanceId); if (worldListTab.isInitialized()) - worldListTab.getNode().loadInstance(repository, version); + worldListTab.getNode().loadInstance(repository, instanceId); if (schematicsTab.isInitialized()) - schematicsTab.getNode().loadInstance(repository, version); - currentVersionUpgradable.set(repository.isModpack(version)); + schematicsTab.getNode().loadInstance(repository, instanceId); + currentInstanceUpgradable.set(repository.isModpack(instanceId)); } private void onNavigated(Navigator.NavigationEvent event) { @@ -176,20 +179,20 @@ private void onNavigated(Navigator.NavigationEvent event) { // If we jumped to game list page and deleted this version // and back to this page, we should return to main page. if (!getRepository().isLoaded() || - !getRepository().hasVersion(getVersion())) { + !getRepository().hasInstance(getInstanceId())) { Platform.runLater(() -> fireEvent(new PageCloseEvent())); return; } - loadVersion(getVersion(), getRepository()); + loadInstance(getInstanceId(), getRepository()); } private void onBrowse(String sub) { - FXUtils.openFolder(getRepository().getRunDirectory(getVersion()).resolve(sub)); + FXUtils.openFolder(getRepository().getRunDirectory(getInstanceId()).resolve(sub)); } private void redownloadAssetIndex() { - Versions.updateGameAssets(getRepository(), getVersion()); + Instances.updateGameAssets(getRepository(), getInstanceId()); } private void clearLibraries() { @@ -224,44 +227,46 @@ private void clearAssets() { } private void clearJunkFiles() { - Versions.cleanVersion(getRepository(), getVersion()); + Instances.cleanInstance(getRepository(), getInstanceId()); } private void testGame() { - Versions.testGame(getRepository(), getVersion()); + Instances.testGame(getRepository(), getInstanceId()); } private void updateGame() { - Versions.updateVersion(getRepository(), getVersion()); + Instances.updateInstance(getRepository(), getInstanceId()); } private void generateLaunchScript() { - Versions.generateLaunchScript(getRepository(), getVersion()); + Instances.generateLaunchScript(getRepository(), getInstanceId()); } private void export() { - Versions.exportVersion(getRepository(), getVersion()); + Instances.exportInstance(getRepository(), getInstanceId()); } private void rename() { - Versions.renameVersion(getRepository(), getVersion()) - .thenApply(newVersionName -> this.preferredVersionName = newVersionName); + Instances.renameInstance(getRepository(), getInstanceId()) + .thenApply(newInstanceId -> this.preferredInstanceId = new GameInstanceID(newInstanceId)); } private void remove() { - Versions.deleteVersion(getRepository(), getVersion()); + Instances.deleteInstance(getRepository(), getInstanceId()); } private void duplicate() { - Versions.duplicateVersion(getRepository(), getVersion()); + Instances.duplicateInstance(getRepository(), getInstanceId()); } public HMCLGameRepository getRepository() { return Optional.ofNullable(instanceReference.get()).map(HMCLGameRepository.InstanceReference::repository).orElse(null); } - public String getVersion() { - return Optional.ofNullable(instanceReference.get()).map(HMCLGameRepository.InstanceReference::instanceId).orElse(null); + public @Nullable GameInstanceID getInstanceId() { + return Optional.ofNullable(instanceReference.get()) + .map(HMCLGameRepository.InstanceReference::instanceId) + .orElse(null); } @Override @@ -274,19 +279,19 @@ public ReadOnlyObjectProperty stateProperty() { return state.getReadOnlyProperty(); } - public static class Skin extends DecoratorAnimatedPageSkin { + public static class Skin extends DecoratorAnimatedPageSkin { /** * Constructor for all SkinBase instances. * * @param control The control for which this Skin should attach to. */ - protected Skin(VersionPage control) { + protected Skin(GameInstancePage control) { super(control); { AdvancedListBox sideBar = new AdvancedListBox() - .addNavigationDrawerTab(control.tab, control.versionSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL) + .addNavigationDrawerTab(control.tab, control.gameSettingsTab, i18n("settings.game"), SVG.SETTINGS, SVG.SETTINGS_FILL) .addNavigationDrawerTab(control.tab, control.installerListTab, i18n("settings.tabs.installers"), SVG.DEPLOYED_CODE, SVG.DEPLOYED_CODE_FILL) .addNavigationDrawerTab(control.tab, control.modListTab, i18n("mods.manage"), SVG.EXTENSION, SVG.EXTENSION_FILL) .addNavigationDrawerTab(control.tab, control.resourcePackTab, i18n("resourcepack.manage"), SVG.TEXTURE) @@ -312,25 +317,25 @@ protected Skin(VersionPage control) { PopupMenu managementList = new PopupMenu(); JFXPopup managementPopup = new JFXPopup(managementList); managementList.getContent().setAll( - new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), control::testGame, managementPopup), - new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), control::generateLaunchScript, managementPopup), + new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch.test"), control::testGame, managementPopup), + new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), control::generateLaunchScript, managementPopup), new MenuSeparator(), - new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), control::rename, managementPopup), - new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), control::duplicate, managementPopup), - new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), control::remove, managementPopup), + new IconedMenuItem(SVG.EDIT, i18n("instance.manage.rename"), control::rename, managementPopup), + new IconedMenuItem(SVG.FOLDER_COPY, i18n("instance.manage.duplicate"), control::duplicate, managementPopup), + new IconedMenuItem(SVG.DELETE, i18n("instance.manage.remove"), control::remove, managementPopup), new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), control::export, managementPopup), new MenuSeparator(), - new IconedMenuItem(null, i18n("version.manage.redownload_assets_index"), control::redownloadAssetIndex, managementPopup), - new IconedMenuItem(null, i18n("version.manage.remove_assets"), control::clearAssets, managementPopup), - new IconedMenuItem(null, i18n("version.manage.remove_libraries"), control::clearLibraries, managementPopup), - new IconedMenuItem(null, i18n("version.manage.clean"), control::clearJunkFiles, managementPopup).addTooltip(i18n("version.manage.clean.tooltip")) + new IconedMenuItem(null, i18n("instance.manage.redownload_assets_index"), control::redownloadAssetIndex, managementPopup), + new IconedMenuItem(null, i18n("instance.manage.remove_assets"), control::clearAssets, managementPopup), + new IconedMenuItem(null, i18n("instance.manage.remove_libraries"), control::clearLibraries, managementPopup), + new IconedMenuItem(null, i18n("instance.manage.clean"), control::clearJunkFiles, managementPopup).addTooltip(i18n("instance.manage.clean.tooltip")) ); AdvancedListBox toolbar = new AdvancedListBox() - .addNavigationDrawerItem(i18n("version.update"), SVG.UPDATE, control::updateGame, upgradeItem -> { - upgradeItem.visibleProperty().bind(control.currentVersionUpgradable); + .addNavigationDrawerItem(i18n("instance.update"), SVG.UPDATE, control::updateGame, upgradeItem -> { + upgradeItem.visibleProperty().bind(control.currentInstanceUpgradable); }) - .addNavigationDrawerItem(i18n("version.launch.test"), SVG.ROCKET_LAUNCH, control::testGame) + .addNavigationDrawerItem(i18n("instance.launch.test"), SVG.ROCKET_LAUNCH, control::testGame) .addNavigationDrawerItem(i18n("settings.game.exploration"), SVG.FOLDER_OPEN, null, browseMenuItem -> { browseMenuItem.setOnAction(e -> browsePopup.show(browseMenuItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, browseMenuItem.getWidth(), 0)); }) @@ -344,7 +349,7 @@ protected Skin(VersionPage control) { } control.state.bind(Bindings.createObjectBinding(() -> - State.fromTitle(i18n("version.manage.manage.title", getSkinnable().getVersion()), -1), + State.fromTitle(i18n("instance.manage.manage.title", getSkinnable().getInstanceId()), -1), getSkinnable().instanceReference)); //control.transitionPane.getStyleClass().add("gray-background"); @@ -359,6 +364,6 @@ public interface GameInstanceLoadable { /// /// @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); + void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameItem.java similarity index 91% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameItem.java index a282be3d018..99594fa77f7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameItem.java @@ -15,11 +15,12 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import javafx.beans.property.*; import javafx.scene.image.Image; import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.modpack.ModpackConfiguration; import org.jackhuang.hmcl.setting.GameDirectory; @@ -44,6 +45,7 @@ public class GameItem { protected final HMCLGameRepository repository; protected final String id; + protected final GameInstanceID instanceId; private boolean initialized = false; private StringProperty title; @@ -51,9 +53,10 @@ public class GameItem { private StringProperty subtitle; private ObjectProperty image; - public GameItem(HMCLGameRepository repository, String id) { + public GameItem(HMCLGameRepository repository, GameInstanceID instanceId) { this.repository = repository; - this.id = id; + this.id = instanceId.toString(); + this.instanceId = instanceId; } public GameDirectory getGameDirectory() { @@ -83,10 +86,10 @@ record Result(@Nullable String gameVersion, @Nullable String tag) { CompletableFuture.supplyAsync(() -> { // GameVersion.minecraftVersion() is a time-costing job (up to ~200 ms) - Optional gameVersion = repository.getGameVersion(id); + Optional gameVersion = repository.getGameVersion(instanceId); String modPackVersion = null; try { - ModpackConfiguration config = repository.readModpackConfiguration(id); + ModpackConfiguration config = repository.readModpackConfiguration(instanceId); modPackVersion = config != null ? config.getVersion() : null; } catch (IOException e) { LOG.warning("Failed to read modpack configuration from " + id, e); @@ -99,7 +102,7 @@ record Result(@Nullable String gameVersion, @Nullable String tag) { } StringBuilder libraries = new StringBuilder(Objects.requireNonNullElse(result.gameVersion, i18n("message.unknown"))); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(id), result.gameVersion); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), result.gameVersion); for (LibraryAnalyzer.LibraryMark mark : analyzer) { String libraryId = mark.getLibraryId(); String libraryVersion = mark.getLibraryVersion(); @@ -118,7 +121,7 @@ record Result(@Nullable String gameVersion, @Nullable String tag) { }, Schedulers.javafx()); title.set(id); - image.set(repository.getVersionIconImage(id)); + image.set(repository.getInstanceIconImage(instanceId)); } public ReadOnlyStringProperty titleProperty() { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java similarity index 88% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java index 5cd185870ab..bb2e3c9907b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListCell.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListCell.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.*; import javafx.beans.property.SimpleStringProperty; @@ -29,6 +29,7 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.construct.*; @@ -69,7 +70,7 @@ public void fire() { fireEvent(new ActionEvent()); GameListItem item = GameListCell.this.getItem(); if (item != null) { - item.getRepository().setSelectedInstance(item.getId()); + item.getRepository().setSelectedInstance(new GameInstanceID(item.getId())); } } } @@ -112,7 +113,7 @@ public void fire() { if (item != null) item.update(); }); - FXUtils.installFastTooltip(btnUpgrade, i18n("version.update")); + FXUtils.installFastTooltip(btnUpgrade, i18n("instance.update")); right.getChildren().add(btnUpgrade); this.btnLaunch = FXUtils.newToggleButton4(SVG.ROCKET_LAUNCH); @@ -122,7 +123,7 @@ public void fire() { item.testGame(); }); BorderPane.setAlignment(btnLaunch, Pos.CENTER); - FXUtils.installFastTooltip(btnLaunch, i18n("version.launch.test")); + FXUtils.installFastTooltip(btnLaunch, i18n("instance.launch.test")); right.getChildren().add(btnLaunch); this.btnManage = FXUtils.newToggleButton4(SVG.MORE_VERT); @@ -190,14 +191,14 @@ private static JFXPopup getPopup(GameListItem item) { JFXPopup popup = new JFXPopup(menu); menu.getContent().setAll( - new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), item::testGame, popup), - new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), item::generateLaunchScript, popup), + new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch.test"), item::testGame, popup), + new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), item::generateLaunchScript, popup), new MenuSeparator(), - new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), item::modifyGameSettings, popup), + new IconedMenuItem(SVG.SETTINGS, i18n("instance.manage.manage"), item::modifyGameSettings, popup), new MenuSeparator(), - new IconedMenuItem(SVG.EDIT, i18n("version.manage.rename"), item::rename, popup), - new IconedMenuItem(SVG.FOLDER_COPY, i18n("version.manage.duplicate"), item::duplicate, popup), - new IconedMenuItem(SVG.DELETE, i18n("version.manage.remove"), item::remove, popup), + new IconedMenuItem(SVG.EDIT, i18n("instance.manage.rename"), item::rename, popup), + new IconedMenuItem(SVG.FOLDER_COPY, i18n("instance.manage.duplicate"), item::duplicate, popup), + new IconedMenuItem(SVG.DELETE, i18n("instance.manage.remove"), item::remove, popup), new IconedMenuItem(SVG.OUTPUT, i18n("modpack.export"), item::export, popup), new MenuSeparator(), new IconedMenuItem(SVG.FOLDER_OPEN, i18n("folder.game"), item::browse, popup)); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListItem.java similarity index 64% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListItem.java index 6fd36783a67..16ae0a4cc62 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListItem.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListItem.java @@ -15,12 +15,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ReadOnlyBooleanProperty; import javafx.beans.property.SimpleBooleanProperty; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.GameDirectoryManager; @@ -30,11 +31,14 @@ public class GameListItem extends GameItem { private final boolean isModpack; private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected"); - public GameListItem(HMCLGameRepository repository, String id) { - super(repository, id); - this.isModpack = repository.isModpack(id); + public GameListItem(HMCLGameRepository repository, GameInstanceID instanceId) { + super(repository, instanceId); + this.isModpack = repository.isModpack(instanceId); selected.bind(Bindings.createBooleanBinding( - () -> repository.getGameDirectory() == GameDirectoryManager.getSelectedGameDirectory() && Objects.equals(repository.getSelectedInstance(), id), + () -> { + if (repository.getGameDirectory() != GameDirectoryManager.getSelectedGameDirectory()) return false; + return Objects.equals(repository.getSelectedInstance(), instanceId); + }, GameDirectoryManager.selectedGameDirectoryProperty(), GameDirectoryManager.selectedInstanceProperty())); } @@ -44,39 +48,39 @@ public ReadOnlyBooleanProperty selectedProperty() { } public void rename() { - Versions.renameVersion(repository, id); + Instances.renameInstance(repository, instanceId); } public void duplicate() { - Versions.duplicateVersion(repository, id); + Instances.duplicateInstance(repository, instanceId); } public void remove() { - Versions.deleteVersion(repository, id); + Instances.deleteInstance(repository, instanceId); } public void export() { - Versions.exportVersion(repository, id); + Instances.exportInstance(repository, instanceId); } public void browse() { - Versions.openFolder(repository, id); + Instances.openFolder(repository, instanceId); } public void testGame() { - Versions.testGame(repository, id); + Instances.testGame(repository, instanceId); } public void launch() { - Versions.launch(repository, id); + Instances.launch(repository, instanceId); } public void modifyGameSettings() { - Versions.modifyGameSettings(repository, id); + Instances.modifyGameSettings(repository, instanceId); } public void generateLaunchScript() { - Versions.generateLaunchScript(repository, id); + Instances.generateLaunchScript(repository, instanceId); } public boolean canUpdate() { @@ -84,6 +88,6 @@ public boolean canUpdate() { } public void update() { - Versions.updateVersion(repository, id); + Instances.updateInstance(repository, instanceId); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPage.java similarity index 95% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPage.java index b83aac3ac5e..7bc9dd5c95b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXListView; @@ -70,7 +70,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class GameListPage extends DecoratorAnimatedPage implements DecoratorPage { - private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("version.manage"))); + private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("instance.manage"))); /// Navigation drawer items for configured game directories. @SuppressWarnings("FieldCanBeLocal") private final ObservableList gameDirectoryListItems; @@ -103,8 +103,8 @@ public GameListPage() { } AdvancedListBox bottomLeftCornerList = new AdvancedListBox() - .addNavigationDrawerItem(i18n("install.new_game"), SVG.ADD_CIRCLE, Versions::addNewGame) - .addNavigationDrawerItem(i18n("install.modpack"), SVG.PACKAGE2, Versions::importModpack) + .addNavigationDrawerItem(i18n("install.new_game"), SVG.ADD_CIRCLE, Instances::addNewGame) + .addNavigationDrawerItem(i18n("install.modpack"), SVG.PACKAGE2, Instances::importModpack) .addNavigationDrawerItem(i18n("settings.type.global.manage"), SVG.SETTINGS, this::modifyGlobalGameSettings); FXUtils.setLimitHeight(bottomLeftCornerList, 40 * 3 + 12 * 2); setLeft(pane, bottomLeftCornerList); @@ -118,13 +118,13 @@ public GameListPage() { if (ModpackHelper.isFileModpackByExtension(file)) { Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(GameDirectoryManager.getSelectedRepository(), file), i18n("install.modpack")); } else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) { - Versions.installFromJson(GameDirectoryManager.getSelectedRepository(), file); + Instances.installFromJson(GameDirectoryManager.getSelectedRepository(), file); } }); } public void modifyGlobalGameSettings() { - Versions.modifyGlobalSettings(GameDirectoryManager.getSelectedRepository()); + Instances.modifyGlobalSettings(GameDirectoryManager.getSelectedRepository()); } @Override @@ -152,12 +152,12 @@ private void loadVersions(HMCLGameRepository repository) { setLoading(true); setFailedReason(null); - List versionItems = repository.getDisplayVersions().map(instance -> new GameListItem(repository, instance.getId())).toList(); + List versionItems = repository.getDisplayInstanceManifests().map(instance -> new GameListItem(repository, instance.id())).toList(); sourceList.setAll(versionItems); if (versionItems.isEmpty()) { - setFailedReason(i18n("version.empty.hint")); + setFailedReason(i18n("instance.empty.hint")); } setLoading(false); @@ -182,7 +182,7 @@ private Predicate createPredicate(String searchText) { } public void refreshList() { - GameDirectoryManager.getSelectedRepository().refreshVersionsAsync().start(); + GameDirectoryManager.getSelectedRepository().refreshAsync().start(); } @Override diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java similarity index 92% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java index 54c16f6a58f..c8c0da1d94f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/GameListPopupMenu.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/GameListPopupMenu.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXListView; import com.jfoenix.controls.JFXPopup; @@ -33,8 +33,9 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.construct.ImageContainer; import org.jackhuang.hmcl.ui.construct.RipplerContainer; @@ -50,11 +51,11 @@ public final class GameListPopupMenu extends StackPane { public static void show(Node owner, JFXPopup.PopupVPosition vAlign, JFXPopup.PopupHPosition hAlign, double initOffsetX, double initOffsetY, - HMCLGameRepository repository, List versions) { + HMCLGameRepository repository, List versions) { GameListPopupMenu menu = new GameListPopupMenu(); menu.getItems().setAll(versions.stream() - .filter(it -> repository.hasVersion(it.getId())) - .map(it -> new GameItem(repository, it.getId())) + .filter(it -> repository.hasInstance(it.id())) + .map(it -> new GameItem(repository, it.id())) .toList()); JFXPopup popup = new JFXPopup(menu); popup.show(owner, vAlign, hAlign, initOffsetX, initOffsetY); @@ -74,7 +75,7 @@ public GameListPopupMenu() { listView.prefHeightProperty().bind(Bindings.size(getItems()).multiply(50).add(2)); - Label placeholder = new Label(i18n("version.empty")); + Label placeholder = new Label(i18n("instance.empty")); placeholder.setStyle("-fx-padding: 10px; -fx-text-fill: -monet-on-surface-variant; -fx-font-style: italic;"); FXUtils.onChangeAndOperate(isEmpty, empty -> { @@ -127,7 +128,7 @@ public Cell(ListView listView) { FXUtils.onClicked(rootPane, () -> { GameItem item = getItem(); if (item != null) { - item.getRepository().setSelectedInstance(item.getId()); + item.getRepository().setSelectedInstance(new GameInstanceID(item.getId())); if (getScene().getWindow() instanceof JFXPopup popup) popup.hide(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/HMCLLocalizedDownloadListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java similarity index 70% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/HMCLLocalizedDownloadListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java index 2c1aa9b61f8..23c43e3e94f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/HMCLLocalizedDownloadListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/HMCLLocalizedDownloadListPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import org.jackhuang.hmcl.game.LocalizedRemoteAddonRepository; import org.jackhuang.hmcl.addon.RemoteAddonRepository; @@ -30,42 +30,42 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; public final class HMCLLocalizedDownloadListPage extends DownloadListPage { - public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS); + public static DownloadListPage ofMod(DownloadPage.DownloadCallback callback, boolean instanceSelection) { + return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, ModrinthRemoteAddonRepository.MODS); } - public static DownloadListPage ofCurseForgeMod(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, null); + public static DownloadListPage ofCurseForgeMod(DownloadPage.DownloadCallback callback, boolean instanceSelection) { + return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddonRepository.Type.MOD, CurseForgeRemoteAddonRepository.MODS, null); } - public static DownloadListPage ofModrinthMod(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MOD, null, ModrinthRemoteAddonRepository.MODS); + public static DownloadListPage ofModrinthMod(DownloadPage.DownloadCallback callback, boolean instanceSelection) { + return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddonRepository.Type.MOD, null, ModrinthRemoteAddonRepository.MODS); } - public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.MODPACK, CurseForgeRemoteAddonRepository.MODPACKS, ModrinthRemoteAddonRepository.MODPACKS); + public static DownloadListPage ofModPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { + return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddonRepository.Type.MODPACK, CurseForgeRemoteAddonRepository.MODPACKS, ModrinthRemoteAddonRepository.MODPACKS); } - public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, ModrinthRemoteAddonRepository.RESOURCE_PACKS); + public static DownloadListPage ofResourcePack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { + return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, ModrinthRemoteAddonRepository.RESOURCE_PACKS); } - public static DownloadListPage ofCurseForgeResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, null); + public static DownloadListPage ofCurseForgeResourcePack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { + return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddonRepository.Type.RESOURCE_PACK, CurseForgeRemoteAddonRepository.RESOURCE_PACKS, null); } - public static DownloadListPage ofModrinthResourcePack(DownloadPage.DownloadCallback callback, boolean versionSelection) { - return new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.RESOURCE_PACK, null, ModrinthRemoteAddonRepository.RESOURCE_PACKS); + public static DownloadListPage ofModrinthResourcePack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { + return new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddonRepository.Type.RESOURCE_PACK, null, ModrinthRemoteAddonRepository.RESOURCE_PACKS); } - public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean versionSelection) { - var page = new HMCLLocalizedDownloadListPage(callback, versionSelection, RemoteAddonRepository.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, ModrinthRemoteAddonRepository.SHADER_PACKS); + public static DownloadListPage ofShaderPack(DownloadPage.DownloadCallback callback, boolean instanceSelection) { + var page = new HMCLLocalizedDownloadListPage(callback, instanceSelection, RemoteAddonRepository.Type.SHADER_PACK, CurseForgeRemoteAddonRepository.SHADERS, ModrinthRemoteAddonRepository.SHADER_PACKS); page.supportChinese.set(false); return page; } - private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean versionSelection, RemoteAddonRepository.Type type, CurseForgeRemoteAddonRepository curseForge, ModrinthRemoteAddonRepository modrinth) { - super(null, callback, versionSelection); + private HMCLLocalizedDownloadListPage(DownloadPage.DownloadCallback callback, boolean instanceSelection, RemoteAddonRepository.Type type, CurseForgeRemoteAddonRepository curseForge, ModrinthRemoteAddonRepository modrinth) { + super(null, callback, instanceSelection); repository = new Repository(type, curseForge, modrinth); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java similarity index 84% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java index ed554baa497..06a5e282434 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/InstallerListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/InstallerListPage.java @@ -15,15 +15,16 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import javafx.application.Platform; import javafx.scene.Node; import javafx.scene.control.Skin; import javafx.stage.FileChooser; import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; @@ -32,6 +33,7 @@ import org.jackhuang.hmcl.ui.download.UpdateInstallerWizardProvider; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; import java.nio.file.Path; import java.util.Arrays; @@ -42,10 +44,10 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; -public class InstallerListPage extends ListPageBase implements VersionPage.GameInstanceLoadable { +public class InstallerListPage extends ListPageBase implements GameInstancePage.GameInstanceLoadable { private HMCLGameRepository repository; - private String versionId; - private Version version; + private GameInstanceID instanceId; + private GameInstanceManifest manifest; private String gameVersion; { @@ -61,16 +63,16 @@ protected Skin createDefaultSkin() { } @Override - public void loadInstance(HMCLGameRepository repository, String instanceId) { + public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) { this.repository = repository; - this.versionId = instanceId; - this.version = repository.getVersion(instanceId); + this.instanceId = instanceId; + this.manifest = repository.getInstanceManifest(instanceId); this.gameVersion = null; CompletableFuture.supplyAsync(() -> { - gameVersion = repository.getGameVersion(version).orElse(null); + gameVersion = repository.getGameVersion(manifest).orElse(null); - return LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(instanceId), gameVersion); + return LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), gameVersion); }).thenAcceptAsync(analyzer -> { itemsProperty().clear(); @@ -98,13 +100,13 @@ public void loadInstance(HMCLGameRepository repository, String instanceId) { } item.setOnInstall(() -> { - Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(repository, gameVersion, version, libraryId, libraryVersion)); + Controllers.getDecorator().startWizard(new UpdateInstallerWizardProvider(repository, gameVersion, manifest, libraryId, libraryVersion)); }); - item.setOnRemove(() -> repository.getDependency().removeLibraryAsync(version, libraryId) + item.setOnRemove(() -> repository.getDependency().removeLibraryAsync(manifest, libraryId) .thenComposeAsync(repository::saveAsync) - .withComposeAsync(repository.refreshVersionsAsync()) - .withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.versionId)) + .withComposeAsync(repository.refreshAsync()) + .withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.instanceId)) .start()); itemsProperty().add(item); @@ -123,10 +125,10 @@ public void loadInstance(HMCLGameRepository repository, String instanceId) { InstallerItem installerItem = new InstallerItem(libraryId, InstallerItem.Style.LIST_ITEM); installerItem.versionProperty().set(new InstallerItem.InstalledState(libraryVersion, false, false)); - installerItem.setOnRemove(() -> repository.getDependency().removeLibraryAsync(version, libraryId) + installerItem.setOnRemove(() -> repository.getDependency().removeLibraryAsync(manifest, libraryId) .thenComposeAsync(repository::saveAsync) - .withComposeAsync(repository.refreshVersionsAsync()) - .withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.versionId)) + .withComposeAsync(repository.refreshAsync()) + .withRunAsync(Schedulers.javafx(), () -> loadInstance(this.repository, this.instanceId)) .start()); itemsProperty().add(installerItem); @@ -142,16 +144,16 @@ public void installOffline() { } private void doInstallOffline(Path file) { - Task task = repository.getDependency().installLibraryAsync(version, file) + Task task = repository.getDependency().installLibraryAsync(manifest, file) .thenComposeAsync(repository::saveAsync) - .thenComposeAsync(repository.refreshVersionsAsync()); + .thenComposeAsync(repository.refreshAsync()); task.setName(i18n("install.installer.install_offline")); TaskExecutor executor = task.executor(new TaskListener() { @Override public void onStop(boolean success, TaskExecutor executor) { runInFX(() -> { if (success) { - loadInstance(repository, versionId); + loadInstance(repository, instanceId); Controllers.dialog(i18n("install.success")); } else { if (executor.getException() == null) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java similarity index 70% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java index c986694163a..520915bda30 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/Versions.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import org.jackhuang.hmcl.setting.GameDirectoryManager; @@ -47,6 +47,7 @@ import org.jackhuang.hmcl.ui.export.ExportWizardProvider; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; +import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -62,8 +63,8 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class Versions { - private Versions() { +public final class Instances { + private Instances() { } public static void addNewGame() { @@ -78,7 +79,7 @@ public static void importModpack() { } } - public static void downloadModpackImpl(DownloadProvider downloadProvider, HMCLGameRepository repository, String version, RemoteAddon mod, RemoteAddon.Version file) { + public static void downloadModpackImpl(DownloadProvider downloadProvider, HMCLGameRepository repository, GameInstanceID instanceId, RemoteAddon mod, RemoteAddon.Version file) { Path modpack; List downloadURLs; try { @@ -95,8 +96,8 @@ public static void downloadModpackImpl(DownloadProvider downloadProvider, HMCLGa .whenComplete(Schedulers.javafx(), e -> { if (e == null) { ModpackInstallWizardProvider installWizardProvider; - if (version != null) - installWizardProvider = new ModpackInstallWizardProvider(repository, modpack, version); + if (instanceId != null) + installWizardProvider = new ModpackInstallWizardProvider(repository, modpack, instanceId); else installWizardProvider = new ModpackInstallWizardProvider(repository, modpack); if (StringUtils.isNotBlank(mod.iconUrl())) @@ -115,19 +116,19 @@ public static void downloadModpackImpl(DownloadProvider downloadProvider, HMCLGa ); } - 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"); + public static void deleteInstance(HMCLGameRepository repository, GameInstanceID instanceId) { + boolean isIndependent = repository.getRunDirectory(instanceId).toAbsolutePath().normalize() + .equals(repository.getInstanceRoot(instanceId).toAbsolutePath().normalize()); + String message = isIndependent ? i18n("instance.manage.remove.confirm.independent", instanceId) : + i18n("instance.manage.remove.confirm.trash", instanceId, instanceId + "_removed"); JFXButton deleteButton = new JFXButton(i18n("button.delete")); deleteButton.getStyleClass().add("dialog-error"); deleteButton.setOnAction(e -> { - Task.supplyAsync(Schedulers.io(), () -> repository.removeVersionFromDisk(version)) + Task.supplyAsync(Schedulers.io(), () -> repository.removeInstanceFromDisk(instanceId)) .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); + Controllers.dialog(i18n("instance.manage.remove.failed"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); } }).start(); }); @@ -135,50 +136,57 @@ public static void deleteVersion(HMCLGameRepository repository, String version) Controllers.confirmAction(message, i18n("message.warning"), MessageDialogPane.MessageType.WARNING, deleteButton); } - public static CompletableFuture renameVersion(HMCLGameRepository repository, String version) { - return Controllers.prompt(i18n("version.manage.rename.message"), (newName, handler) -> { - if (newName.equals(version)) { + public static CompletableFuture renameInstance(HMCLGameRepository repository, GameInstanceID instanceId) { + return Controllers.prompt(i18n("instance.manage.rename.message"), (newName, handler) -> { + if (newName.equals(instanceId.toString())) { handler.resolve(); return; } - if (repository.renameVersion(version, newName)) { + GameInstanceID oldInstanceId = instanceId; + GameInstanceID newInstanceId = new GameInstanceID(newName); + if (repository.renameInstance(oldInstanceId, newInstanceId)) { handler.resolve(); - repository.refreshVersionsAsync() + repository.refreshAsync() .thenRunAsync(Schedulers.javafx(), () -> { - if (repository.hasVersion(newName)) { - repository.setSelectedInstance(newName); + if (repository.hasInstance(newInstanceId)) { + repository.setSelectedInstance(newInstanceId); } }).start(); } else { - handler.reject(i18n("version.manage.rename.fail")); + handler.reject(i18n("instance.manage.rename.fail")); } - }, version, - new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidVersionId), - new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.versionIdConflicts(newVersionName) || newVersionName.equals(version))); + }, instanceId.toString(), + new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId), + new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName) || newVersionName.equals(instanceId.toString()))); } - public static void exportVersion(HMCLGameRepository repository, String version) { - Controllers.getDecorator().startWizard(new ExportWizardProvider(repository, version), i18n("modpack.wizard")); + public static void exportInstance(HMCLGameRepository repository, GameInstanceID instanceId) { + Controllers.getDecorator().startWizard(new ExportWizardProvider(repository, instanceId), i18n("modpack.wizard")); } - public static void openFolder(HMCLGameRepository repository, String version) { - FXUtils.openFolder(repository.getRunDirectory(version)); + public static void openFolder(HMCLGameRepository repository, GameInstanceID instanceId) { + FXUtils.openFolder(repository.getRunDirectory(instanceId)); } public static void installFromJson(HMCLGameRepository repository, Path file) { - Version version; + GameInstanceManifest manifest; try { - version = repository.readVersionJson(file); + manifest = JsonUtils.fromJsonFile(file, GameInstanceManifest.class); + if (manifest == null) { + throw new IllegalArgumentException("Missing game manifest"); + } } catch (Exception e) { Controllers.dialog(i18n("install.new_game.malformed_json"), i18n("message.error"), MessageDialogPane.MessageType.ERROR); return; } - Controllers.prompt(i18n("version.manage.duplicate.prompt"), (result, handler) -> { + Controllers.prompt(i18n("instance.manage.duplicate.prompt"), (result, handler) -> { handler.resolve(); + GameInstanceID instanceId = new GameInstanceID(result); + DefaultDependencyManager dependencyManager = repository.getDependency(); - Version newVersion = version.setId(result).setJar(result); + GameInstanceManifest newVersion = manifest.withId(instanceId).withJar(instanceId); Controllers.taskDialog( Task.allOf(new GameDownloadTask(dependencyManager, null, newVersion), @@ -189,71 +197,72 @@ public static void installFromJson(HMCLGameRepository repository, Path file) { // ignore failure })) .thenComposeAsync(repository.saveAsync(newVersion)) - .thenRunAsync(repository::refreshVersions) + .thenRunAsync(repository::refresh) .whenComplete(Schedulers.javafx(), (exception) -> { if (exception == null) { - repository.setSelectedInstance(result); + repository.setSelectedInstance(new GameInstanceID(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 -> !repository.versionIdConflicts(newVersionName))); + }, FileUtils.getNameWithoutExtension(file), new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId), new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName))); } - public static void duplicateVersion(HMCLGameRepository repository, String version) { + public static void duplicateInstance(HMCLGameRepository repository, GameInstanceID instanceId) { Controllers.prompt( - new PromptDialogPane.Builder(i18n("version.manage.duplicate.prompt"), (res, handler) -> { - String newVersionName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue(); + new PromptDialogPane.Builder(i18n("instance.manage.duplicate.prompt"), (res, handler) -> { + String newInstanceName = ((PromptDialogPane.Builder.StringQuestion) res.get(1)).getValue(); + GameInstanceID newInstanceId = new GameInstanceID(newInstanceName); boolean copySaves = ((PromptDialogPane.Builder.BooleanQuestion) res.get(2)).getValue(); - Task.runAsync(() -> repository.duplicateVersion(version, newVersionName, copySaves)) - .thenComposeAsync(repository.refreshVersionsAsync()) + Task.runAsync(() -> repository.duplicateInstance(instanceId, newInstanceId, copySaves)) + .thenComposeAsync(repository.refreshAsync()) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { handler.resolve(); } else { handler.reject(StringUtils.getStackTrace(exception)); - if (!repository.versionIdConflicts(newVersionName)) { - repository.removeVersionFromDisk(newVersionName); + if (!repository.instanceIdConflicts(newInstanceId)) { + repository.removeInstanceFromDisk(newInstanceId); } } }).start(); }) - .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 -> !repository.versionIdConflicts(newVersionName)))) - .addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false))); + .addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("instance.manage.duplicate.confirm"))) + .addQuestion(new PromptDialogPane.Builder.StringQuestion(null, instanceId.toString(), + new Validator(i18n("install.new_game.malformed"), HMCLGameRepository::isValidInstanceId), + new Validator(i18n("install.new_game.already_exists"), newVersionName -> !repository.instanceIdConflicts(newVersionName)))) + .addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("instance.manage.duplicate.duplicate_save"), false))); } - public static void updateVersion(HMCLGameRepository repository, String version) { - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository, version)); + public static void updateInstance(HMCLGameRepository repository, GameInstanceID instanceId) { + Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository, instanceId)); } - public static void updateGameAssets(HMCLGameRepository repository, String version) { - TaskExecutor executor = new GameAssetDownloadTask(repository.getDependency(), repository.getVersion(version), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true) + public static void updateGameAssets(HMCLGameRepository repository, GameInstanceID instanceId) { + TaskExecutor executor = new GameAssetDownloadTask(repository.getDependency(), repository.getInstanceManifest(instanceId), GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true) .executor(); - Controllers.taskDialog(executor, i18n("version.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL); + Controllers.taskDialog(executor, i18n("instance.manage.redownload_assets_index"), TaskCancellationAction.NO_CANCEL); executor.start(); } - public static void cleanVersion(HMCLGameRepository repository, String id) { + public static void cleanInstance(HMCLGameRepository repository, GameInstanceID instanceId) { try { - repository.clean(id); + repository.clean(instanceId); } catch (IOException e) { LOG.warning("Unable to clean game directory", e); } } @SafeVarargs - public static void generateLaunchScript(HMCLGameRepository repository, String id, Consumer... injecters) { - if (!checkVersionForLaunching(repository, id)) + public static void generateLaunchScript(HMCLGameRepository repository, GameInstanceID instanceId, Consumer... injecters) { + if (!checkVersionForLaunching(repository, instanceId)) return; ensureSelectedAccount(account -> { FileChooser chooser = new FileChooser(); - if (Files.isDirectory(repository.getRunDirectory(id))) - chooser.setInitialDirectory(repository.getRunDirectory(id).toFile()); - chooser.setTitle(i18n("version.launch_script.save")); + if (Files.isDirectory(repository.getRunDirectory(instanceId))) + chooser.setInitialDirectory(repository.getRunDirectory(instanceId).toFile()); + chooser.setTitle(i18n("instance.launch_script.save")); if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) { chooser.getExtensionFilters().add( new FileChooser.ExtensionFilter(i18n("extension.command"), "*.command") @@ -270,7 +279,7 @@ public static void generateLaunchScript(HMCLGameRepository repository, String id file = file.resolveSibling(file.getFileName().toString() + "." + defaultExt); } - LauncherHelper launcherHelper = new LauncherHelper(repository, account, id); + LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId); for (Consumer injecter : injecters) { injecter.accept(launcherHelper); } @@ -295,11 +304,11 @@ private static String getDefaultScriptExtension() { } @SafeVarargs - public static void launch(HMCLGameRepository repository, String id, Consumer... injecters) { - if (!checkVersionForLaunching(repository, id)) + public static void launch(HMCLGameRepository repository, GameInstanceID instanceId, Consumer... injecters) { + if (!checkVersionForLaunching(repository, instanceId)) return; ensureSelectedAccount(account -> { - LauncherHelper launcherHelper = new LauncherHelper(repository, account, id); + LauncherHelper launcherHelper = new LauncherHelper(repository, account, instanceId); for (Consumer injecter : injecters) { injecter.accept(launcherHelper); } @@ -307,27 +316,34 @@ public static void launch(HMCLGameRepository repository, String id, Consumer + public static void launchAndEnterWorld(HMCLGameRepository repository, GameInstanceID instanceId, String worldFolderName) { + launch(repository, instanceId, launcherHelper -> launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName))); } - public static void generateLaunchScriptForQuickEnterWorld(HMCLGameRepository repository, String id, String worldFolderName) { - generateLaunchScript(repository, id, launcherHelper -> + public static void generateLaunchScriptForQuickEnterWorld(HMCLGameRepository repository, GameInstanceID instanceId, String worldFolderName) { + generateLaunchScript(repository, instanceId, launcherHelper -> launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName))); } - 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")); + private static boolean checkVersionForLaunching(HMCLGameRepository repository, GameInstanceID instanceId) { + boolean unavailable; + if (instanceId == null || !repository.isLoaded()) { + unavailable = true; + } else { + unavailable = !repository.hasInstance(instanceId); + } + + if (unavailable) { + JFXButton gotoDownload = new JFXButton(i18n("instance.empty.launch.goto_download")); gotoDownload.getStyleClass().add("dialog-accept"); gotoDownload.setOnAction(e -> Controllers.navigate(Controllers.getDownloadPage())); - Controllers.confirmAction(i18n("version.empty.launch"), i18n("launch.failed"), + Controllers.confirmAction(i18n("instance.empty.launch"), i18n("launch.failed"), MessageDialogPane.MessageType.ERROR, gotoDownload, null); @@ -372,10 +388,10 @@ public static void modifyGlobalSettings(HMCLGameRepository repository) { Controllers.navigate(Controllers.getSettingsPage()); } - public static void modifyGameSettings(HMCLGameRepository repository, String version) { - Controllers.getVersionPage().setVersion(version, repository); - Controllers.getVersionPage().showInstanceSettings(); + public static void modifyGameSettings(HMCLGameRepository repository, GameInstanceID instanceId) { + Controllers.getGameInstancePage().setInstance(instanceId, repository); + Controllers.getGameInstancePage().showInstanceSettings(); // VersionPage.loadVersion will be invoked after navigation - Controllers.navigate(Controllers.getVersionPage()); + Controllers.navigate(Controllers.getGameInstancePage()); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java similarity index 94% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java index 5207aab98e7..5852840242d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; @@ -23,8 +23,9 @@ import javafx.scene.control.Skin; import javafx.stage.FileChooser; import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.addon.mod.LocalModFile; import org.jackhuang.hmcl.addon.mod.ModLoaderType; import org.jackhuang.hmcl.addon.mod.ModManager; @@ -39,6 +40,7 @@ import org.jackhuang.hmcl.ui.construct.PageAware; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.io.FileUtils; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.io.UncheckedIOException; @@ -51,14 +53,14 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class ModListPage extends ListPageBase implements VersionPage.GameInstanceLoadable, PageAware { +public final class ModListPage extends ListPageBase implements GameInstancePage.GameInstanceLoadable, PageAware { private final BooleanProperty modded = new SimpleBooleanProperty(this, "modded", false); private final ReentrantLock lock = new ReentrantLock(); private ModManager modManager; private HMCLGameRepository repository; - private String instanceId; + private GameInstanceID instanceId; private String gameVersion; final EnumSet supportedLoaders = EnumSet.noneOf(ModLoaderType.class); @@ -86,11 +88,11 @@ public void refresh() { } @Override - public void loadInstance(HMCLGameRepository repository, String instanceId) { + public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) { this.repository = repository; this.instanceId = instanceId; - Version resolved = repository.getResolvedPreservingPatchesVersion(instanceId); + GameInstanceManifest resolved = repository.getResolvedInstanceManifest(instanceId).standaloneManifest(); this.gameVersion = repository.getGameVersion(resolved).orElse(null); LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion); modded.set(analyzer.hasModLoader()); @@ -265,7 +267,7 @@ public void checkUpdates(Collection mods) { } public void download() { - Controllers.getDownloadPage().showModDownloads().selectVersion(instanceId); + Controllers.getDownloadPage().showModDownloads().selectInstance(instanceId); Controllers.navigate(Controllers.getDownloadPage()); } @@ -298,7 +300,7 @@ public HMCLGameRepository getRepository() { return this.repository; } - public String getInstanceId() { + public GameInstanceID getInstanceId() { return this.instanceId; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPageSkin.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPageSkin.java index 10ac770a365..da62b941cf8 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModListPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModListPageSkin.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.*; import javafx.animation.PauseTransition; @@ -44,7 +44,7 @@ import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.DownloadProviders; -import org.jackhuang.hmcl.setting.VersionIconType; +import org.jackhuang.hmcl.setting.GameInstanceIconType; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.ui.Controllers; @@ -343,7 +343,7 @@ private Image loadIcon() { LOG.warning("Failed to load mod icons", e); } - return VersionIconType.getIconType(this.localModFile.getModLoaderType()).getIcon(); + return GameInstanceIconType.getIconType(this.localModFile.getModLoaderType()).getIcon(); } public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference> current) { @@ -359,7 +359,7 @@ public void loadIcon(ImageContainer imageContainer, @Nullable WeakReference(imageFuture); } - imageContainer.setImage(VersionIconType.getIconType(localModFile.getModLoaderType()).getIcon()); + imageContainer.setImage(GameInstanceIconType.getIconType(localModFile.getModLoaderType()).getIcon()); imageFuture.thenAcceptAsync(image -> { if (current != null) { ObjectProperty infoObjectProperty = current.get(); @@ -543,7 +543,7 @@ final class ModInfoListCell extends MDListCell { content.setMouseTransparent(true); setSelectable(); - imageContainer.setImage(VersionIconType.COMMAND.getIcon()); + imageContainer.setImage(GameInstanceIconType.COMMAND.getIcon()); FXUtils.installFastTooltip(restoreButton, i18n("mods.restore")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModTranslations.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModTranslations.java index e7bb8328d46..c19d97f729c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ModTranslations.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ModTranslations.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import org.jackhuang.hmcl.addon.RemoteAddonRepository; import org.jackhuang.hmcl.util.Pair; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java index 504cc0bfce5..3d377e31624 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/ResourcePackListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/ResourcePackListPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.*; import javafx.animation.PauseTransition; @@ -42,6 +42,7 @@ 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.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.setting.SettingsManager; import org.jackhuang.hmcl.setting.DownloadProviders; @@ -74,7 +75,7 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class ResourcePackListPage extends ListPageBase implements VersionPage.GameInstanceLoadable { +public final class ResourcePackListPage extends ListPageBase implements GameInstancePage.GameInstanceLoadable { private static final String TIP_KEY = "resourcePackWarning"; private static @Nullable String getWarning(ResourcePackFile.Compatibility compatibility) { @@ -89,7 +90,7 @@ public final class ResourcePackListPage extends ListPageBase createDefaultSkin() { } @Override - public void loadInstance(HMCLGameRepository repository, String instanceId) { + public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) { this.repository = repository; this.instanceId = instanceId; this.resourcePackManager = new ResourcePackManager(repository, instanceId); @@ -183,7 +184,7 @@ public void onAddFiles() { } private void onDownload() { - Controllers.getDownloadPage().showResourcePackDownloads().selectVersion(instanceId); + Controllers.getDownloadPage().showResourcePackDownloads().selectInstance(instanceId); Controllers.navigate(Controllers.getDownloadPage()); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/SchematicsPage.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/SchematicsPage.java index 241c47a4351..24617c50e51 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/SchematicsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/SchematicsPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXDialogLayout; @@ -34,6 +34,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.schematic.LitematicFile; import org.jackhuang.hmcl.task.Schedulers; @@ -62,7 +63,7 @@ /** * @author Glavo */ -public final class SchematicsPage extends ListPageBase implements VersionPage.GameInstanceLoadable { +public final class SchematicsPage extends ListPageBase implements GameInstancePage.GameInstanceLoadable { private static String translateAuthorName(String author) { if (I18n.isUseChinese() && "hsds".equals(author)) { @@ -87,7 +88,7 @@ protected Skin createDefaultSkin() { } @Override - public void loadInstance(HMCLGameRepository repository, String instanceId) { + public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) { this.schematicsDirectory = repository.getSchematicsDirectory(instanceId); refresh(); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupTask.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupTask.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupTask.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupTask.java index 79d704bd902..a51e956a6d0 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupTask.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupTask.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.task.Task; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupsPage.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupsPage.java index 76a41ce665f..7fae552882d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldBackupsPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldBackupsPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import javafx.beans.property.BooleanProperty; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPage.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPage.java index d7fee2c1510..d0161eb609c 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPageSkin.java similarity index 98% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPageSkin.java index 1430b4b41ce..2255c4da10d 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldExportPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldExportPageSkin.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXTextField; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldInfoPage.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldInfoPage.java index 2c508aebae7..b04162855ad 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldInfoPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldInfoPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXTextField; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldListPage.java similarity index 95% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldListPage.java index 66c06609196..dae93f97a5b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldListPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldListPage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXCheckBox; @@ -35,6 +35,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.StackPane; import javafx.stage.FileChooser; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.task.Schedulers; @@ -45,6 +46,7 @@ import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.nio.file.FileAlreadyExistsException; @@ -61,13 +63,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public final class WorldListPage extends ListPageBase implements VersionPage.GameInstanceLoadable { +public final class WorldListPage extends ListPageBase implements GameInstancePage.GameInstanceLoadable { private final BooleanProperty showAll = new SimpleBooleanProperty(this, "showAll", false); private Path savesDir; private List worlds; private HMCLGameRepository repository; - private String instanceId; + private GameInstanceID instanceId; private final BooleanProperty supportQuickPlay = new SimpleBooleanProperty(this, "supportQuickPlay", false); private int refreshCount = 0; @@ -86,7 +88,7 @@ protected Skin createDefaultSkin() { } @Override - public void loadInstance(HMCLGameRepository repository, String instanceId) { + public void loadInstance(HMCLGameRepository repository, @Nullable GameInstanceID instanceId) { this.repository = repository; this.instanceId = instanceId; this.savesDir = repository.getSavesDirectory(instanceId); @@ -197,11 +199,11 @@ public void reveal(World world) { } public void launch(World world) { - Versions.launchAndEnterWorld(repository, instanceId, world.getFileName()); + Instances.launchAndEnterWorld(repository, instanceId, world.getFileName()); } public void generateLaunchScript(World world) { - Versions.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName()); + Instances.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName()); } public BooleanProperty showAllProperty() { @@ -280,7 +282,7 @@ public WorldListCell(WorldListPage page) { btnLaunch.visibleProperty().bind(page.supportQuickPlayProperty()); btnLaunch.managedProperty().bind(btnLaunch.visibleProperty()); right.getChildren().add(btnLaunch); - FXUtils.installFastTooltip(btnLaunch, i18n("version.launch")); + FXUtils.installFastTooltip(btnLaunch, i18n("instance.launch")); btnLaunch.setOnAction(event -> { World world = getItem(); if (world != null) @@ -354,12 +356,12 @@ public void showPopupMenu(World world, boolean supportQuickPlay, JFXPopup.PopupH if (supportQuickPlay) { - IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), () -> page.launch(world), popup); + IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch_and_enter_world"), () -> page.launch(world), popup); launchItem.setDisable(worldLocked); popupMenu.getContent().addAll( launchItem, - new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> page.generateLaunchScript(world), popup), + new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), () -> page.generateLaunchScript(world), popup), new MenuSeparator() ); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManagePage.java similarity index 94% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManagePage.java index 68399a745cd..fcc89fb16d9 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManagePage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManagePage.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import com.jfoenix.controls.JFXPopup; import javafx.application.Platform; @@ -24,6 +24,7 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.ui.Controllers; @@ -54,7 +55,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco private final World world; private final Path backupsDir; private final HMCLGameRepository repository; - private final String instanceId; + private final GameInstanceID instanceId; private final boolean supportQuickPlay; private FileChannel sessionLockChannel; @@ -69,7 +70,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco private final TabHeader.Tab worldBackupsTab = new TabHeader.Tab<>("worldBackupsPage"); private final TabHeader.Tab dataPackTab = new TabHeader.Tab<>("dataPackListPage"); - public WorldManagePage(World world, HMCLGameRepository repository, String instanceId) { + public WorldManagePage(World world, HMCLGameRepository repository, GameInstanceID instanceId) { this.world = world; this.backupsDir = repository.getBackupsDirectory(instanceId); this.repository = repository; @@ -150,11 +151,11 @@ public void onExited(Navigator.NavigationEvent event) { public void launch() { fireEvent(new PageCloseEvent()); - Versions.launchAndEnterWorld(repository, instanceId, world.getFileName()); + Instances.launchAndEnterWorld(repository, instanceId, world.getFileName()); } public void generateLaunchScript() { - Versions.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName()); + Instances.generateLaunchScriptForQuickEnterWorld(repository, instanceId, world.getFileName()); } @Override @@ -232,7 +233,7 @@ private AdvancedListBox getToolBar() { BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); { if (getSkinnable().supportQuickPlay) { - toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty())); + toolbar.addNavigationDrawerItem(i18n("instance.launch"), SVG.ROCKET_LAUNCH, () -> getSkinnable().launch(), advancedListItem -> advancedListItem.disableProperty().bind(getSkinnable().readOnlyProperty())); } if (ChunkBaseApp.isSupported(getSkinnable().world)) { @@ -265,8 +266,8 @@ private AdvancedListBox getToolBar() { if (getSkinnable().supportQuickPlay) { managePopupMenu.getContent().addAll( - new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch"), () -> getSkinnable().launch(), managePopup), - new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup), + new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("instance.launch"), () -> getSkinnable().launch(), managePopup), + new IconedMenuItem(SVG.SCRIPT, i18n("instance.launch_script"), () -> getSkinnable().generateLaunchScript(), managePopup), new MenuSeparator() ); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManageUIUtils.java similarity index 99% rename from HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java rename to HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManageUIUtils.java index c99e083d2d6..179946b4bad 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/versions/WorldManageUIUtils.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/WorldManageUIUtils.java @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.jackhuang.hmcl.ui.versions; +package org.jackhuang.hmcl.ui.instances; import javafx.stage.FileChooser; import org.jackhuang.hmcl.game.World; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java index 3248ee2f9af..f9af5a9893b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/MainPage.java @@ -44,8 +44,9 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.VersionList; +import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.HMCLGameRepository; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.setting.DownloadProviders; import org.jackhuang.hmcl.setting.GameDirectory; import org.jackhuang.hmcl.setting.GameDirectoryManager; @@ -61,8 +62,8 @@ import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.TwoLineListItem; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.ui.versions.GameListPopupMenu; -import org.jackhuang.hmcl.ui.versions.Versions; +import org.jackhuang.hmcl.ui.instances.GameListPopupMenu; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.upgrade.RemoteVersion; import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.upgrade.UpdateHandler; @@ -72,6 +73,7 @@ import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.Platform; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jetbrains.annotations.Nullable; import java.io.IOException; import java.util.List; @@ -90,11 +92,11 @@ public final class MainPage extends StackPane implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(); - private final StringProperty currentGame = new SimpleStringProperty(this, "currentGame"); + private final ObjectProperty<@Nullable GameInstanceID> currentGame = new SimpleObjectProperty<>(this, "currentGame"); private final BooleanProperty showUpdate = new SimpleBooleanProperty(this, "showUpdate"); private final BooleanProperty showUpdateDialog = new SimpleBooleanProperty(this, "showUpdateDialog"); private final ObjectProperty latestVersion = new SimpleObjectProperty<>(this, "latestVersion"); - private final ObservableList versions = FXCollections.observableArrayList(); + private final ObservableList versions = FXCollections.observableArrayList(); private HMCLGameRepository repository; private TransitionPane announcementPane; @@ -208,9 +210,9 @@ public final class MainPage extends StackPane implements DecoratorPage { HBox launchPane = new HBox(); launchPane.getStyleClass().add("launch-pane"); FXUtils.onScroll(launchPane, versions, list -> { - String currentId = getCurrentGame(); - return Lang.indexWhere(list, instance -> instance.getId().equals(currentId)); - }, it -> repository.setSelectedInstance(it.getId())); + GameInstanceID currentId = getCurrentGame(); + return Lang.indexWhere(list, instance -> instance.id().equals(currentId)); + }, it -> repository.setSelectedInstance(it.id())); StackPane.setAlignment(launchPane, Pos.BOTTOM_RIGHT); { @@ -229,18 +231,18 @@ public final class MainPage extends StackPane implements DecoratorPage { private Tooltip tooltip; @Override - public void accept(String currentGame) { + public void accept(@Nullable GameInstanceID currentGame) { if (currentGame == null) { - launchLabel.setText(i18n("version.launch.empty")); + launchLabel.setText(i18n("instance.launch.empty")); currentLabel.setText(null); graphic.getChildren().setAll(launchLabel); FXUtils.setOnActionWithCooldown(launchButton, MainPage.this::launchNoGame); if (tooltip == null) - tooltip = new Tooltip(i18n("version.launch.empty.tooltip")); + tooltip = new Tooltip(i18n("instance.launch.empty.tooltip")); FXUtils.installFastTooltip(launchButton, tooltip); } else { - launchLabel.setText(i18n("version.launch")); - currentLabel.setText(currentGame); + launchLabel.setText(i18n("instance.launch")); + currentLabel.setText(currentGame.toString()); graphic.getChildren().setAll(launchLabel, currentLabel); FXUtils.setOnActionWithCooldown(launchButton, MainPage.this::launch); if (tooltip != null) @@ -262,7 +264,7 @@ public void accept(String currentGame) { -menuButton.getHeight(), repository, versions )); - FXUtils.installFastTooltip(menuButton, i18n("version.switch")); + FXUtils.installFastTooltip(menuButton, i18n("instance.switch")); menuButton.setGraphic(SVG.ARROW_DROP_UP.createIcon(30)); EventHandler secondaryClickHandle = event -> { @@ -317,14 +319,14 @@ private void doAnimation(boolean show) { private void launch() { HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); - Versions.launch(repository, repository.getSelectedInstance()); + Instances.launch(repository, repository.getSelectedInstance()); } private void launchNoGame() { DownloadProvider downloadProvider = DownloadProviders.getDownloadProvider(); VersionList versionList = downloadProvider.getVersionListById("game"); - Holder gameVersionHolder = new Holder<>(); + Holder instanceHolder = new Holder<>(); Task task = versionList.refreshAsync("") .thenSupplyAsync(() -> versionList.getVersions("").stream() .filter(it -> it.getVersionType() == RELEASE) @@ -335,17 +337,21 @@ private void launchNoGame() { .thenComposeAsync(version -> { HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); DefaultDependencyManager dependency = repository.getDependency(); - String gameVersion = gameVersionHolder.value = version.getGameVersion(); - return dependency.gameBuilder() - .name(gameVersion) + String gameVersion = version.getGameVersion(); + GameInstanceID instanceId = new GameInstanceID(gameVersion); + + instanceHolder.value = instanceId; + + return dependency.newGameBuilder() + .name(instanceId) .gameVersion(gameVersion) .buildAsync(); }) - .whenComplete(any -> GameDirectoryManager.getSelectedRepository().refreshVersions()) + .whenComplete(any -> GameDirectoryManager.getSelectedRepository().refresh()) .whenComplete(Schedulers.javafx(), (result, exception) -> { if (exception == null) { - GameDirectoryManager.getSelectedRepository().setSelectedInstance(gameVersionHolder.value); + GameDirectoryManager.getSelectedRepository().setSelectedInstance(instanceHolder.value); launch(); } else if (exception instanceof CancellationException) { Controllers.showToast(i18n("message.cancelled")); @@ -356,7 +362,7 @@ private void launchNoGame() { MessageDialogPane.MessageType.WARNING); } }); - Controllers.taskDialog(task, i18n("version.launch.empty.installing"), TaskCancellationAction.NORMAL); + Controllers.taskDialog(task, i18n("instance.launch.empty.installing"), TaskCancellationAction.NORMAL); } private void onUpgrade() { @@ -385,19 +391,19 @@ public HMCLGameRepository getRepository() { return repository; } - public String getCurrentGame() { + public GameInstanceID getCurrentGame() { return currentGame.get(); } - public StringProperty currentGameProperty() { + public ObjectProperty<@Nullable GameInstanceID> currentGameProperty() { return currentGame; } - public void setCurrentGame(String currentGame) { + public void setCurrentGame(@Nullable GameInstanceID currentGame) { this.currentGame.set(currentGame); } - public ObservableList getVersions() { + public ObservableList getVersions() { return versions; } @@ -437,7 +443,7 @@ public void setLatestVersion(RemoteVersion latestVersion) { this.latestVersion.set(latestVersion); } - public void initVersions(HMCLGameRepository repository, List versions) { + public void initVersions(HMCLGameRepository repository, List versions) { FXUtils.checkFxUserThread(); this.repository = repository; this.versions.setAll(versions); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index 01c41530244..97478cf94f5 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -22,10 +22,11 @@ import javafx.scene.layout.Region; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.event.EventBus; -import org.jackhuang.hmcl.event.RefreshedVersionsEvent; +import org.jackhuang.hmcl.event.RefreshedGameInstancesEvent; +import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameInstanceManifest; 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.GameDirectory; import org.jackhuang.hmcl.setting.GameDirectoryManager; @@ -46,9 +47,9 @@ import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; import org.jackhuang.hmcl.ui.nbt.NBTEditorPage; import org.jackhuang.hmcl.ui.nbt.NBTFileType; -import org.jackhuang.hmcl.ui.versions.GameAdvancedListItem; -import org.jackhuang.hmcl.ui.versions.GameListPopupMenu; -import org.jackhuang.hmcl.ui.versions.Versions; +import org.jackhuang.hmcl.ui.instances.GameAdvancedListItem; +import org.jackhuang.hmcl.ui.instances.GameListPopupMenu; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.upgrade.UpdateChecker; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; @@ -74,7 +75,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { private MainPage mainPage = null; public RootPage() { - EventBus.EVENT_BUS.channel(RefreshedVersionsEvent.class) + EventBus.EVENT_BUS.channel(RefreshedGameInstancesEvent.class) .register(event -> onRefreshedVersions((HMCLGameRepository) event.getSource())); HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); @@ -115,7 +116,7 @@ public MainPage getMainPage() { i18n("message.error"), MessageDialogPane.MessageType.ERROR); } } else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) { - Versions.installFromJson(GameDirectoryManager.getSelectedRepository(), file); + Instances.installFromJson(GameDirectoryManager.getSelectedRepository(), file); } }); @@ -124,11 +125,11 @@ public MainPage getMainPage() { GameDirectoryManager.registerVersionsListener(repository -> { GameDirectory gameDirectory = repository.getGameDirectory(); - List children = repository.getVersions().parallelStream() + List children = repository.getInstanceManifests().parallelStream() .filter(version -> !version.isHidden()) .sorted(Comparator - .comparing((Version version) -> Lang.requireNonNullElse(version.getReleaseTime(), Instant.EPOCH)) - .thenComparing(version -> VersionNumber.asVersion(repository.getGameVersion(version).orElse(version.getId())))) + .comparing((GameInstanceManifest manifest) -> Lang.requireNonNullElse(manifest.releaseTime(), Instant.EPOCH)) + .thenComparing(manifest -> VersionNumber.asVersion(repository.getGameVersion(manifest).orElse(manifest.id().toString())))) .collect(Collectors.toList()); runInFX(() -> { if (gameDirectory == GameDirectoryManager.getSelectedGameDirectory()) @@ -154,26 +155,26 @@ protected Skin(RootPage control) { // second item in left sidebar GameAdvancedListItem gameListItem = new GameAdvancedListItem(); gameListItem.setOnAction(e -> { - String version = GameDirectoryManager.getSelectedRepository().getSelectedInstance(); - if (version == null) { + GameInstanceID instanceId = GameDirectoryManager.getSelectedRepository().getSelectedInstance(); + if (instanceId == null) { Controllers.navigate(Controllers.getGameListPage()); } else { - Versions.modifyGameSettings(GameDirectoryManager.getSelectedRepository(), version); + Instances.modifyGameSettings(GameDirectoryManager.getSelectedRepository(), instanceId); } }); FXUtils.onScroll(gameListItem, getSkinnable().getMainPage().getVersions(), list -> { - String currentId = getSkinnable().getMainPage().getCurrentGame(); - return Lang.indexWhere(list, instance -> instance.getId().equals(currentId)); - }, it -> getSkinnable().getMainPage().getRepository().setSelectedInstance(it.getId())); + GameInstanceID currentId = getSkinnable().getMainPage().getCurrentGame(); + return Lang.indexWhere(list, instance -> instance.id().equals(currentId)); + }, it -> getSkinnable().getMainPage().getRepository().setSelectedInstance(it.id())); if (AnimationUtils.isAnimationEnabled()) { - FXUtils.prepareOnMouseEnter(gameListItem, Controllers::prepareVersionPage); + FXUtils.prepareOnMouseEnter(gameListItem, Controllers::prepareGameInstancePage); } FXUtils.onSecondaryButtonClicked(gameListItem, () -> showGameListPopupMenu(gameListItem)); // third item in left sidebar AdvancedListItem gameItem = new AdvancedListItem(); gameItem.setLeftIcon(SVG.FORMAT_LIST_BULLETED); - gameItem.setTitle(i18n("version.manage")); + gameItem.setTitle(i18n("instance.manage")); gameItem.setOnAction(e -> Controllers.navigate(Controllers.getGameListPage())); FXUtils.onSecondaryButtonClicked(gameItem, () -> showGameListPopupMenu(gameItem)); @@ -228,7 +229,7 @@ else if (Platform.SYSTEM_PLATFORM.equals(OperatingSystem.LINUX, Architecture.LOO AdvancedListBox sideBar = new AdvancedListBox() .startCategory(i18n("account").toUpperCase(Locale.ROOT)) .add(accountListItem) - .startCategory(i18n("version").toUpperCase(Locale.ROOT)) + .startCategory(i18n("instance").toUpperCase(Locale.ROOT)) .add(gameListItem) .add(gameItem) .add(downloadItem) @@ -263,7 +264,7 @@ private void onRefreshedVersions(HMCLGameRepository repository) { if (!checkedModpack) { checkedModpack = true; - if (repository.getVersionCount() == 0) { + if (repository.getInstanceCount() == 0) { Path zipModpack = Metadata.CURRENT_DIRECTORY.resolve("modpack.zip"); Path mrpackModpack = Metadata.CURRENT_DIRECTORY.resolve("modpack.mrpack"); @@ -280,7 +281,7 @@ private void onRefreshedVersions(HMCLGameRepository repository) { Task.supplyAsync(() -> CompressingUtils.findSuitableEncoding(modpackFile)) .thenApplyAsync(encoding -> ModpackHelper.readModpackManifest(modpackFile, encoding)) .thenApplyAsync(modpack -> ModpackHelper - .getInstallTask(repository, modpackFile, modpack.getName(), modpack, null) + .getInstallTask(repository, modpackFile, new GameInstanceID(modpack.getName()), modpack, null) .executor()) .thenAcceptAsync(Schedulers.javafx(), executor -> { Controllers.taskDialog(executor, i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java index c2a015eb2e6..7880fe3f27e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaControllerPage.java @@ -51,7 +51,7 @@ import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; -import org.jackhuang.hmcl.ui.versions.Versions; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.i18n.LocaleUtils; import org.jackhuang.hmcl.util.io.FileUtils; @@ -210,9 +210,9 @@ public TerracottaControllerPage() { i18n("terracotta.status.waiting.host.launch.desc"), i18n("terracotta.status.waiting.host.launch.title"), MessageDialogPane.MessageType.QUESTION - ).addAction(i18n("version.launch"), () -> { + ).addAction(i18n("instance.launch"), () -> { var repository = GameDirectoryManager.getSelectedRepository(); - Versions.launch(repository, repository.getSelectedInstance(), launcherHelper -> { + Instances.launch(repository, repository.getSelectedInstance(), launcherHelper -> { launcherHelper.setKeep(); launcherHelper.setDisableOfflineSkin(); }); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaPage.java index 4f5f15f384c..e4ea34fda53 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/terracotta/TerracottaPage.java @@ -25,6 +25,7 @@ import javafx.scene.layout.BorderPane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.terracotta.TerracottaMetadata; import org.jackhuang.hmcl.ui.Controllers; @@ -37,10 +38,9 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; import org.jackhuang.hmcl.ui.main.MainPage; -import org.jackhuang.hmcl.ui.versions.GameListPopupMenu; -import org.jackhuang.hmcl.ui.versions.Versions; +import org.jackhuang.hmcl.ui.instances.GameListPopupMenu; +import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.StringUtils; import static org.jackhuang.hmcl.setting.SettingsManager.userState; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; @@ -54,7 +54,7 @@ public class TerracottaPage extends DecoratorAnimatedPage implements DecoratorPa private final TransitionPane transitionPane = new TransitionPane(); @SuppressWarnings("unused") - private ChangeListener instanceChangeListenerHolder; + private ChangeListener instanceChangeListenerHolder; public TerracottaPage() { statusPage.setNodeSupplier(TerracottaControllerPage::new); @@ -77,22 +77,22 @@ public TerracottaPage() { AdvancedListBox toolbar = new AdvancedListBox() .add(accountListItem) - .addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, () -> { + .addNavigationDrawerItem(i18n("instance.launch"), SVG.ROCKET_LAUNCH, () -> { var repository = GameDirectoryManager.getSelectedRepository(); - Versions.launch(repository, repository.getSelectedInstance(), launcherHelper -> { + Instances.launch(repository, repository.getSelectedInstance(), launcherHelper -> { launcherHelper.setKeep(); launcherHelper.setDisableOfflineSkin(); }); }, item -> { instanceChangeListenerHolder = FXUtils.onWeakChangeAndOperate(GameDirectoryManager.selectedInstanceProperty(), - instanceName -> item.setSubtitle(StringUtils.isNotBlank(instanceName) ? instanceName : i18n("version.empty")) + instanceName -> item.setSubtitle(instanceName != null ? instanceName.toString() : i18n("instance.empty")) ); MainPage mainPage = Controllers.getRootPage().getMainPage(); FXUtils.onScroll(item, mainPage.getVersions(), list -> { - String currentId = mainPage.getCurrentGame(); - return Lang.indexWhere(list, instance -> instance.getId().equals(currentId)); - }, it -> mainPage.getRepository().setSelectedInstance(it.getId())); + GameInstanceID currentId = mainPage.getCurrentGame(); + return Lang.indexWhere(list, instance -> instance.id().equals(currentId)); + }, it -> mainPage.getRepository().setSelectedInstance(it.id())); FXUtils.onSecondaryButtonClicked(item, () -> GameListPopupMenu.show(item, JFXPopup.PopupVPosition.BOTTOM, diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java b/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java index a5019d739b5..bf6ecc8e131 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/util/NativePatcher.java @@ -64,8 +64,8 @@ private static Map getNatives(Platform platform) { } // https://github.com/LWJGL/lwjgl3/issues/1111 - public static boolean needPatchMemoryUtil(Version version, int javaVersion) { - return javaVersion >= 25 && javaVersion <= 26 && version.getLibraries().stream().anyMatch(library -> + public static boolean needPatchMemoryUtil(GameInstanceManifest manifest, int javaVersion) { + return javaVersion >= 25 && javaVersion <= 26 && manifest.getLibraries().stream().anyMatch(library -> "org.lwjgl".equals(library.getGroupId()) && "lwjgl".equals(library.getArtifactId()) && "3.4.1".equals(library.getVersion()) @@ -73,17 +73,17 @@ public static boolean needPatchMemoryUtil(Version version, int javaVersion) { ); } - public static Version patchNative(DefaultGameRepository repository, - Version version, String gameVersion, - JavaRuntime javaVersion, - GameSettings.Effective settings, - List javaArguments) { + public static GameInstanceManifest patchNative(DefaultGameRepository repository, + GameInstanceManifest manifest, String gameVersion, + JavaRuntime javaVersion, + GameSettings.Effective settings, + List javaArguments) { if (settings.get(GameSettings::useCustomNativesProperty)) { if (gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") < 0) - return version; + return manifest; ArrayList newLibraries = new ArrayList<>(); - for (Library library : version.getLibraries()) { + for (Library library : manifest.getLibraries()) { if (!library.appliesToCurrentEnvironment()) continue; @@ -93,7 +93,7 @@ public static Version patchNative(DefaultGameRepository repository, newLibraries.add(library); } } - return version.setLibraries(newLibraries); + return manifest.withLibraries(newLibraries); } final boolean useNativeGLFW = settings.get(GameSettings::useNativeGLFWProperty); @@ -102,7 +102,7 @@ public static Version patchNative(DefaultGameRepository repository, if (OperatingSystem.CURRENT_OS.isLinuxOrBSD() && (useNativeGLFW || useNativeOpenAL) && gameVersion != null && GameVersionNumber.compare(gameVersion, "1.19") >= 0) { - version = version.setLibraries(version.getLibraries().stream() + manifest = manifest.withLibraries(manifest.getLibraries().stream() .filter(library -> { if (library.getClassifier() != null && library.getClassifier().startsWith("natives") && "org.lwjgl".equals(library.getGroupId())) { @@ -125,25 +125,25 @@ public static Version patchNative(DefaultGameRepository repository, GameVersionNumber gameVersionNumber = gameVersion != null ? GameVersionNumber.asGameVersion(gameVersion) : null; if (settings.get(GameSettings::notPatchNativesProperty)) - return version; + return manifest; if (arch.isX86() && (os == OperatingSystem.WINDOWS || os == OperatingSystem.LINUX || os == OperatingSystem.MACOS)) - return version; + return manifest; if (arch == Architecture.ARM64 && (os == OperatingSystem.MACOS || os == OperatingSystem.WINDOWS) && gameVersionNumber != null && gameVersionNumber.compareTo("1.19") >= 0) - return version; + return manifest; Map replacements = getNatives(javaVersion.getPlatform()); if (replacements.isEmpty()) { LOG.warning("No alternative native library provided for platform " + javaVersion.getPlatform()); - return version; + return manifest; } boolean lwjglVersionChanged = false; ArrayList newLibraries = new ArrayList<>(); - for (Library library : version.getLibraries()) { + for (Library library : manifest.getLibraries()) { if (!library.appliesToCurrentEnvironment()) continue; @@ -172,7 +172,7 @@ public static Version patchNative(DefaultGameRepository repository, } if (lwjglVersionChanged) { - ModManager modManager = repository.getModManager(version.getId()); + ModManager modManager = repository.getModManager(manifest.id()); try { for (LocalModFile mod : modManager.getLocalFiles()) { if ("sodium".equals(mod.getId())) { @@ -186,7 +186,7 @@ public static Version patchNative(DefaultGameRepository repository, } } - return version.setLibraries(newLibraries); + return manifest.withLibraries(newLibraries); } /// @see Java Mesa Loader for Windows diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index e1365723a82..ae3385c9925 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -816,6 +816,58 @@ install.new_game.malformed_json=Invalid Minecraft instance JSON file. install.select=Choose operation install.success=Successfully installed. +instance=Games +instance.name=Instance Name +instance.empty=No Instances +instance.empty.add=Add new instance +instance.empty.launch=No available instances.\nYou can go to the "Download" page to get the game, or switch the game directory in the "All Instances" page. +instance.empty.launch.goto_download=Go to Download Page +instance.empty.hint=There are no Minecraft instances here.\nYou can try switching to another game directory or clicking here to download one. +instance.game.all=All +instance.game.april_fools=April Fools +instance.game.old=Historical +instance.game.release=Release +instance.game.releases=Releases +instance.game.snapshot=Snapshot +instance.game.snapshots=Snapshots +instance.game.support_status.unsupported=Unsupported +instance.game.support_status.untested=Untested +instance.game.type=Type +instance.launch=Launch Game +instance.launch_and_enter_world=Play World +instance.launch.empty=Start Game +instance.launch.empty.installing=Installing Game +instance.launch.empty.tooltip=Install and launch the latest official release +instance.launch.test=Test Launch +instance.switch=Switch Instance +instance.launch_script=Export Launch Script +instance.launch_script.failed=Failed to export launch script. +instance.launch_script.save=Export Launch Script +instance.launch_script.success=Exported launch script as %s. +instance.manage=All Instances +instance.manage.clean=Delete Log Files +instance.manage.clean.tooltip=Delete the files in "logs" and "crash-reports" directories. +instance.manage.duplicate=Duplicate Instance +instance.manage.duplicate.duplicate_save=Duplicate Worlds +instance.manage.duplicate.prompt=Enter New Instance Name +instance.manage.duplicate.confirm=The duplicated instance will have a copy of all files in the instance directory (".minecraft/versions/"), with an isolated working directory and settings. +instance.manage.manage=Edit Instance +instance.manage.manage.title=Edit Instance - %1s +instance.manage.redownload_assets_index=Update Game Assets +instance.manage.remove=Delete Instance +instance.manage.remove.confirm.trash=Are you sure you want to remove the instance "%1$s"? You can still find its files in your recycle bin by the name of "%2$s". +instance.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its worlds and other data. Do you still want to delete the instance "%s"? +instance.manage.remove.failed=Failed to delete the instance. Some files might be in use. +instance.manage.remove_assets=Delete All Assets +instance.manage.remove_libraries=Delete All Libraries +instance.manage.rename=Rename Instance +instance.manage.rename.message=Enter New Instance Name +instance.manage.rename.fail=Failed to rename the instance. Some files might be in use, or the name contains an invalid character. +instance.search=Name +instance.search.prompt=Enter the version name to search +instance.settings=Settings +instance.update=Update Modpack + java.add=Add java.add.failed=This Java is invalid or incompatible with the current platform. java.disable=Disable Java @@ -1800,59 +1852,6 @@ update.tooltip=Update update.preview=Preview HMCL releases early update.preview.subtitle=Enable this option to receive new versions of HMCL early for testing before their official release. -version=Games -version.name=Instance Name -version.cannot_read=Failed to parse the game instance, installation cannot continue. -version.empty=No Instances -version.empty.add=Add new instance -version.empty.launch=No available instances.\nYou can go to the "Download" page to get the game, or switch the game directory in the "All Instances" page. -version.empty.launch.goto_download=Go to Download Page -version.empty.hint=There are no Minecraft instances here.\nYou can try switching to another game directory or clicking here to download one. -version.game.all=All -version.game.april_fools=April Fools -version.game.old=Historical -version.game.release=Release -version.game.releases=Releases -version.game.snapshot=Snapshot -version.game.snapshots=Snapshots -version.game.support_status.unsupported=Unsupported -version.game.support_status.untested=Untested -version.game.type=Type -version.launch=Launch Game -version.launch_and_enter_world=Play World -version.launch.empty=Start Game -version.launch.empty.installing=Installing Game -version.launch.empty.tooltip=Install and launch the latest official release -version.launch.test=Test Launch -version.switch=Switch Instance -version.launch_script=Export Launch Script -version.launch_script.failed=Failed to export launch script. -version.launch_script.save=Export Launch Script -version.launch_script.success=Exported launch script as %s. -version.manage=All Instances -version.manage.clean=Delete Log Files -version.manage.clean.tooltip=Delete the files in "logs" and "crash-reports" directories. -version.manage.duplicate=Duplicate Instance -version.manage.duplicate.duplicate_save=Duplicate Worlds -version.manage.duplicate.prompt=Enter New Instance Name -version.manage.duplicate.confirm=The duplicated instance will have a copy of all files in the instance directory (".minecraft/versions/"), with an isolated working directory and settings. -version.manage.manage=Edit Instance -version.manage.manage.title=Edit Instance - %1s -version.manage.redownload_assets_index=Update Game Assets -version.manage.remove=Delete Instance -version.manage.remove.confirm.trash=Are you sure you want to remove the instance "%1$s"? You can still find its files in your recycle bin by the name of "%2$s". -version.manage.remove.confirm.independent=Since this instance is stored in an isolated directory, deleting it will also delete its worlds and other data. Do you still want to delete the instance "%s"? -version.manage.remove.failed=Failed to delete the instance. Some files might be in use. -version.manage.remove_assets=Delete All Assets -version.manage.remove_libraries=Delete All Libraries -version.manage.rename=Rename Instance -version.manage.rename.message=Enter New Instance Name -version.manage.rename.fail=Failed to rename the instance. Some files might be in use, or the name contains an invalid character. -version.search=Name -version.search.prompt=Enter the version name to search -version.settings=Settings -version.update=Update Modpack - warning.java_interpreted_mode=HMCL is running in an interpreted Java environment, which will greatly affect performance.\n\ \n\ We recommend using a Java with JIT support to open HMCL for the best experience. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ar.properties b/HMCL/src/main/resources/assets/lang/I18N_ar.properties index 58144270e0b..5008d630997 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ar.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ar.properties @@ -779,6 +779,58 @@ install.new_game.malformed=اسم غير صالح. install.select=اختر العملية install.success=تم التثبيت بنجاح. +instance=الألعاب +instance.name=اسم النسخة +instance.empty=لا توجد نسخ +instance.empty.add=إضافة نسخة جديدة +instance.empty.launch=لا توجد نسخ متاحة.\nيمكنك الانتقال إلى صفحة "التنزيل" للحصول على اللعبة، أو تغيير مجلد اللعبة في صفحة "جميع النسخ". +instance.empty.launch.goto_download=الانتقال إلى صفحة التنزيل +instance.empty.hint=لا توجد نسخ Minecraft هنا.\nيمكنك محاولة التبديل إلى مجلد لعبة آخر أو النقر هنا لتنزيل واحدة. +instance.game.all=الكل +instance.game.april_fools=كذبة أبريل +instance.game.old=تاريخي +instance.game.release=رسمي +instance.game.releases=الإصدارات الرسمية +instance.game.snapshot=تجريبي +instance.game.snapshots=الإصدارات التجريبية +instance.game.support_status.unsupported=غير مدعوم +instance.game.support_status.untested=غير مختبر +instance.game.type=النوع +instance.launch=تشغيل اللعبة +instance.launch_and_enter_world=لعب في العالم +instance.launch.empty=تشغيل اللعبة +instance.launch.empty.installing=جارٍ تثبيت اللعبة +instance.launch.empty.tooltip=تثبيت وتشغيل أحدث إصدار رسمي +instance.launch.test=تشغيل تجريبي +instance.switch=تبديل النسخة +instance.launch_script=تصدير سكريبت التشغيل +instance.launch_script.failed=فشل تصدير سكريبت التشغيل. +instance.launch_script.save=تصدير سكريبت التشغيل +instance.launch_script.success=تم تصدير سكريبت التشغيل باسم %s. +instance.manage=جميع النسخ +instance.manage.clean=حذف ملفات السجلات +instance.manage.clean.tooltip=حذف الملفات في مجلدي "logs" و"crash-reports". +instance.manage.duplicate=نسخ النسخة +instance.manage.duplicate.duplicate_save=نسخ العوالم +instance.manage.duplicate.prompt=أدخل اسم النسخة الجديدة +instance.manage.duplicate.confirm=ستحتوي النسخة المنسوخة على نسخة من جميع الملفات في مجلد النسخة (".minecraft/versions/<اسم النسخة>")، مع مجلد عمل وإعدادات معزولة. +instance.manage.manage=تعديل النسخة +instance.manage.manage.title=تعديل النسخة - %1s +instance.manage.redownload_assets_index=تحديث أصول اللعبة +instance.manage.remove=حذف النسخة +instance.manage.remove.confirm.trash=هل أنت متأكد من إزالة النسخة "%1$s"؟ لا يزال بإمكانك إيجاد ملفاتها في سلة المحذوفات باسم "%2$s". +instance.manage.remove.confirm.independent=نظراً لأن هذه النسخة مخزَّنة في مجلد معزول، فإن حذفها سيحذف أيضاً عوالمها وبياناتها الأخرى. هل تريد حذف النسخة "%s"؟ +instance.manage.remove.failed=فشل حذف النسخة. قد تكون بعض الملفات قيد الاستخدام. +instance.manage.remove_assets=حذف جميع الأصول +instance.manage.remove_libraries=حذف جميع المكتبات +instance.manage.rename=إعادة تسمية النسخة +instance.manage.rename.message=أدخل اسم النسخة الجديدة +instance.manage.rename.fail=فشل إعادة تسمية النسخة. قد تكون بعض الملفات قيد الاستخدام، أو يحتوي الاسم على حرف غير صالح. +instance.search=الاسم +instance.search.prompt=أدخل اسم النسخة للبحث +instance.settings=الإعدادات +instance.update=تحديث حزمة المودات + java.add=إضافة java.add.failed=هذا Java غير صالح أو غير متوافق مع المنصة الحالية. java.disable=تعطيل Java @@ -1566,59 +1618,6 @@ update.tooltip=تحديث update.preview=الاطلاع المبكر على إصدارات HMCL update.preview.subtitle=فعّل هذا الخيار لاستقبال إصدارات HMCL الجديدة مبكراً للاختبار قبل إصدارها الرسمي. -version=الألعاب -version.name=اسم النسخة -version.cannot_read=فشل تحليل نسخة اللعبة، لا يمكن متابعة التثبيت. -version.empty=لا توجد نسخ -version.empty.add=إضافة نسخة جديدة -version.empty.launch=لا توجد نسخ متاحة.\nيمكنك الانتقال إلى صفحة "التنزيل" للحصول على اللعبة، أو تغيير مجلد اللعبة في صفحة "جميع النسخ". -version.empty.launch.goto_download=الانتقال إلى صفحة التنزيل -version.empty.hint=لا توجد نسخ Minecraft هنا.\nيمكنك محاولة التبديل إلى مجلد لعبة آخر أو النقر هنا لتنزيل واحدة. -version.game.all=الكل -version.game.april_fools=كذبة أبريل -version.game.old=تاريخي -version.game.release=رسمي -version.game.releases=الإصدارات الرسمية -version.game.snapshot=تجريبي -version.game.snapshots=الإصدارات التجريبية -version.game.support_status.unsupported=غير مدعوم -version.game.support_status.untested=غير مختبر -version.game.type=النوع -version.launch=تشغيل اللعبة -version.launch_and_enter_world=لعب في العالم -version.launch.empty=تشغيل اللعبة -version.launch.empty.installing=جارٍ تثبيت اللعبة -version.launch.empty.tooltip=تثبيت وتشغيل أحدث إصدار رسمي -version.launch.test=تشغيل تجريبي -version.switch=تبديل النسخة -version.launch_script=تصدير سكريبت التشغيل -version.launch_script.failed=فشل تصدير سكريبت التشغيل. -version.launch_script.save=تصدير سكريبت التشغيل -version.launch_script.success=تم تصدير سكريبت التشغيل باسم %s. -version.manage=جميع النسخ -version.manage.clean=حذف ملفات السجلات -version.manage.clean.tooltip=حذف الملفات في مجلدي "logs" و"crash-reports". -version.manage.duplicate=نسخ النسخة -version.manage.duplicate.duplicate_save=نسخ العوالم -version.manage.duplicate.prompt=أدخل اسم النسخة الجديدة -version.manage.duplicate.confirm=ستحتوي النسخة المنسوخة على نسخة من جميع الملفات في مجلد النسخة (".minecraft/versions/<اسم النسخة>")، مع مجلد عمل وإعدادات معزولة. -version.manage.manage=تعديل النسخة -version.manage.manage.title=تعديل النسخة - %1s -version.manage.redownload_assets_index=تحديث أصول اللعبة -version.manage.remove=حذف النسخة -version.manage.remove.confirm.trash=هل أنت متأكد من إزالة النسخة "%1$s"؟ لا يزال بإمكانك إيجاد ملفاتها في سلة المحذوفات باسم "%2$s". -version.manage.remove.confirm.independent=نظراً لأن هذه النسخة مخزَّنة في مجلد معزول، فإن حذفها سيحذف أيضاً عوالمها وبياناتها الأخرى. هل تريد حذف النسخة "%s"؟ -version.manage.remove.failed=فشل حذف النسخة. قد تكون بعض الملفات قيد الاستخدام. -version.manage.remove_assets=حذف جميع الأصول -version.manage.remove_libraries=حذف جميع المكتبات -version.manage.rename=إعادة تسمية النسخة -version.manage.rename.message=أدخل اسم النسخة الجديدة -version.manage.rename.fail=فشل إعادة تسمية النسخة. قد تكون بعض الملفات قيد الاستخدام، أو يحتوي الاسم على حرف غير صالح. -version.search=الاسم -version.search.prompt=أدخل اسم النسخة للبحث -version.settings=الإعدادات -version.update=تحديث حزمة المودات - warning.java_interpreted_mode=يعمل HMCL في بيئة Java مُترجمة تفسيرياً، مما سيؤثر بشكل كبير على الأداء.\n\ \n\ نوصي باستخدام Java يدعم JIT لفتح HMCL للحصول على أفضل تجربة. diff --git a/HMCL/src/main/resources/assets/lang/I18N_es.properties b/HMCL/src/main/resources/assets/lang/I18N_es.properties index 6e93385e37d..bb706948328 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_es.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_es.properties @@ -743,6 +743,51 @@ install.new_game.malformed=Nombre no válido. install.select=Elegir una operación install.success=Instalado con éxito. +instance=Juegos +instance.name=Nombre de instancia +instance.empty=No hay instancias +instance.empty.add=Añadir una instancia +instance.empty.hint=No hay instancias de Minecraft aquí.\nPuedes intentar cambiar a otro directorio del juego o hacer clic aquí para descargar una. +instance.game.all=Todos +instance.game.april_fools=Día de los Inocentes +instance.game.old=Histórico +instance.game.release=Lanzamiento +instance.game.releases=Lanzamientos +instance.game.snapshot=Snapshot +instance.game.snapshots=Snapshots +instance.game.type=Tipo +instance.launch=Iniciar Minecraft +instance.launch.empty=Iniciar Minecraft +instance.launch.empty.installing=Instalando el juego +instance.launch.empty.tooltip=Instala y ejecuta la última versión oficial del juego. +instance.launch.test=Probar instalación +instance.switch=Cambiar Instancia +instance.launch_script=Exportar script de ejecución +instance.launch_script.failed=No se ha podido exportar el script de ejecución. +instance.launch_script.save=Exportar script de ejecución +instance.launch_script.success=Exportado el script de ejecución como %s. +instance.manage=Todas las instancias +instance.manage.clean=Eliminar archivos de registros +instance.manage.clean.tooltip=Eliminar los archivos de los directorios «logs» y «crash-reports». +instance.manage.duplicate=Duplicar instancia +instance.manage.duplicate.duplicate_save=Duplicar mundos +instance.manage.duplicate.prompt=Introduzca el nombre de la nueva instancia +instance.manage.duplicate.confirm=La instancia duplicada tendrá una copia de todos los archivos de esta instancia, con un directorio de juego y una configuración aislados. +instance.manage.manage=Editar instancia +instance.manage.manage.title=Editar instancia - %1s +instance.manage.redownload_assets_index=Actualizar activos del juego +instance.manage.remove=Eliminar instancia +instance.manage.remove.confirm.trash=¿Estás seguro de que quieres eliminar la instancia «%s»? Todavía puedes encontrar sus archivos en tu papelera de reciclaje con el nombre de «%s». +instance.manage.remove_assets=Borrar todas las activos del juego +instance.manage.remove_libraries=Borrar todas las bibliotecas +instance.manage.rename=Renombrar instancia +instance.manage.rename.message=Introduzca el nombre de la nueva instancia +instance.manage.rename.fail=No se ha podido renombrar la instancia, algunos archivos pueden estar en uso o el nombre contiene un carácter no válido. +instance.search=Nombre +instance.search.prompt=Introduzca el nombre de la versión para buscar. +instance.settings=Configuración +instance.update=Actualizar Modpack + java.add.failed=Este Java no es válido o es incompatible con la plataforma actual. java.disable=Deshabilitar este Java java.disable.confirm=¿Está seguro de que desea desactivar este Java? @@ -1379,52 +1424,6 @@ update.tooltip=Actualización update.preview=Vista previa de actualizaciones anticipadas update.preview.subtitle=Activa esta opción para recibir nuevas versiones del lanzador antes de su lanzamiento oficial para probarlas -version=Juegos -version.name=Nombre de instancia -version.cannot_read=No se ha podido analizar la instancia del juego, la instalación no puede continuar. -version.empty=No hay instancias -version.empty.add=Añadir una instancia -version.empty.hint=No hay instancias de Minecraft aquí.\nPuedes intentar cambiar a otro directorio del juego o hacer clic aquí para descargar una. -version.game.all=Todos -version.game.april_fools=Día de los Inocentes -version.game.old=Histórico -version.game.release=Lanzamiento -version.game.releases=Lanzamientos -version.game.snapshot=Snapshot -version.game.snapshots=Snapshots -version.game.type=Tipo -version.launch=Iniciar Minecraft -version.launch.empty=Iniciar Minecraft -version.launch.empty.installing=Instalando el juego -version.launch.empty.tooltip=Instala y ejecuta la última versión oficial del juego. -version.launch.test=Probar instalación -version.switch=Cambiar Instancia -version.launch_script=Exportar script de ejecución -version.launch_script.failed=No se ha podido exportar el script de ejecución. -version.launch_script.save=Exportar script de ejecución -version.launch_script.success=Exportado el script de ejecución como %s. -version.manage=Todas las instancias -version.manage.clean=Eliminar archivos de registros -version.manage.clean.tooltip=Eliminar los archivos de los directorios «logs» y «crash-reports». -version.manage.duplicate=Duplicar instancia -version.manage.duplicate.duplicate_save=Duplicar mundos -version.manage.duplicate.prompt=Introduzca el nombre de la nueva instancia -version.manage.duplicate.confirm=La instancia duplicada tendrá una copia de todos los archivos de esta instancia, con un directorio de juego y una configuración aislados. -version.manage.manage=Editar instancia -version.manage.manage.title=Editar instancia - %1s -version.manage.redownload_assets_index=Actualizar activos del juego -version.manage.remove=Eliminar instancia -version.manage.remove.confirm.trash=¿Estás seguro de que quieres eliminar la instancia «%s»? Todavía puedes encontrar sus archivos en tu papelera de reciclaje con el nombre de «%s». -version.manage.remove_assets=Borrar todas las activos del juego -version.manage.remove_libraries=Borrar todas las bibliotecas -version.manage.rename=Renombrar instancia -version.manage.rename.message=Introduzca el nombre de la nueva instancia -version.manage.rename.fail=No se ha podido renombrar la instancia, algunos archivos pueden estar en uso o el nombre contiene un carácter no válido. -version.search=Nombre -version.search.prompt=Introduzca el nombre de la versión para buscar. -version.settings=Configuración -version.update=Actualizar Modpack - warning.java_interpreted_mode=El lanzador se ejecuta en un entorno Java interpretado, lo que afectará considerablemente al rendimiento.\n\ \n\ Recomendamos utilizar Java con soporte JIT para ejecutar el lanzador y obtener la mejor experiencia. diff --git a/HMCL/src/main/resources/assets/lang/I18N_ja.properties b/HMCL/src/main/resources/assets/lang/I18N_ja.properties index e8c7856aa8e..faa409ba033 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ja.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ja.properties @@ -476,6 +476,42 @@ install.new_game.malformed=無効な名前 install.select=操作を選択します install.success=正常にインストールされました +instance=ゲーム +instance.empty=ゲームなし +instance.empty.add=起動構成を作成 +instance.empty.hint=インストールされているゲームバージョンはありません。新しいゲームをダウンロードするには、ここをクリックしてください。 +instance.game.old=Old +instance.game.release=リリース +instance.game.releases=リリース +instance.game.snapshot=スナップショット +instance.game.snapshots=Snapshots +instance.launch=プレイ +instance.launch.test=テストプレイ +instance.switch=バージョンを切り替える +instance.launch_script=起動スクリプトを作成する +instance.launch_script.failed=起動スクリプトを作成できません。 +instance.launch_script.save=起動スクリプトを保存する +instance.launch_script.success=作成されたスクリプト: %s。 +instance.manage=起動構成 +instance.manage.clean=ゲームディレクトリをクリアする +instance.manage.clean.tooltip=ログのクリア、クラッシュレポート +instance.manage.duplicate=ゲームインスタンスをコピーする +instance.manage.duplicate.duplicate_save=コピー保存 +instance.manage.duplicate.prompt=新しいバージョン名を入力してください +instance.manage.duplicate.confirm= +instance.manage.manage=バージョンの管理 +instance.manage.manage.title=バージョンの管理 - %1s +instance.manage.redownload_assets_index=ゲームアセットファイルの更新 +instance.manage.remove=このバージョンを削除します +instance.manage.remove.confirm.trash=このバージョン %s を削除してもよろしいですか?このバージョンは、システムのゴミ箱に %s という名前で復元できます。 +instance.manage.remove_assets=すべてのゲームリソースファイルの削除 +instance.manage.remove_libraries=ライブラリファイルを削除します +instance.manage.rename=このバージョンの名前を変更します +instance.manage.rename.message=このバージョンの新しい名前を入力してください +instance.manage.rename.fail=バージョンの名前を変更できませんでした。 +instance.settings=設定 +instance.update=modpackを更新する + java.add.failed=このJavaは無効であるか、現在のプラットフォームと互換性がない。 java.disable=無効化 java.disable.confirm=本当にこのJavaを無効にしますか? @@ -902,43 +938,6 @@ update.latest=これは最新バージョンです。 update.no_browser=ブラウザを開くことができません。リンクがクリップボードにコピーされました。ブラウザのアドレスバーに貼り付けて更新します。 update.tooltip=更新 -version=ゲーム -version.cannot_read=ゲームのバージョンが見つかりません。自動インストールを続行できません。 -version.empty=ゲームなし -version.empty.add=起動構成を作成 -version.empty.hint=インストールされているゲームバージョンはありません。新しいゲームをダウンロードするには、ここをクリックしてください。 -version.game.old=Old -version.game.release=リリース -version.game.releases=リリース -version.game.snapshot=スナップショット -version.game.snapshots=Snapshots -version.launch=プレイ -version.launch.test=テストプレイ -version.switch=バージョンを切り替える -version.launch_script=起動スクリプトを作成する -version.launch_script.failed=起動スクリプトを作成できません。 -version.launch_script.save=起動スクリプトを保存する -version.launch_script.success=作成されたスクリプト: %s。 -version.manage=起動構成 -version.manage.clean=ゲームディレクトリをクリアする -version.manage.clean.tooltip=ログのクリア、クラッシュレポート -version.manage.duplicate=ゲームインスタンスをコピーする -version.manage.duplicate.duplicate_save=コピー保存 -version.manage.duplicate.prompt=新しいバージョン名を入力してください -version.manage.duplicate.confirm= -version.manage.manage=バージョンの管理 -version.manage.manage.title=バージョンの管理 - %1s -version.manage.redownload_assets_index=ゲームアセットファイルの更新 -version.manage.remove=このバージョンを削除します -version.manage.remove.confirm.trash=このバージョン %s を削除してもよろしいですか?このバージョンは、システムのゴミ箱に %s という名前で復元できます。 -version.manage.remove_assets=すべてのゲームリソースファイルの削除 -version.manage.remove_libraries=ライブラリファイルを削除します -version.manage.rename=このバージョンの名前を変更します -version.manage.rename.message=このバージョンの新しい名前を入力してください -version.manage.rename.fail=バージョンの名前を変更できませんでした。 -version.settings=設定 -version.update=modpackを更新する - wiki.version.game=https://ja.minecraft.wiki/w/Java_Edition_%s wiki.version.game.snapshot=https://ja.minecraft.wiki/w/Java_Edition_%s diff --git a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties index 7c8c5eb4f3a..c8f7afb618f 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_lzh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_lzh.properties @@ -559,6 +559,52 @@ install.new_game.malformed=逾矩之名 install.select=擇一裝法 install.success=裝成 +instance=戲 +instance.name=戲例之名 +instance.empty=無游戲例 +instance.empty.add=入引頁以裝戲 +instance.empty.hint=無畢裝者。\n君且更他戲夾,抑擊斯以往戲之引頁。 +instance.game.all=全覽 +instance.game.april_fools=愚人節 +instance.game.old=古典版 +instance.game.release=當版 +instance.game.releases=當版 +instance.game.snapshot=預版 +instance.game.snapshots=預版 +instance.game.type=版類 +instance.launch=戲 +instance.launch.empty=啟戲 +instance.launch.empty.installing=裝戲 +instance.launch.empty.tooltip=裝啟至新之當版 +instance.launch.test=試其戲 +instance.switch=迭例 +instance.launch_script=成啟戲角本 +instance.launch_script.failed=弗能成啟戲角本 +instance.launch_script.save=存啟戲角本 +instance.launch_script.success=啟戲角本既成:%s +instance.manage=例列 +instance.manage.clean=掃戲之案夾 +instance.manage.clean.tooltip=除空「logs」與「crash-reports」案夾 +instance.manage.duplicate=鈔戲例 +instance.manage.duplicate.duplicate_save=鈔生界 +instance.manage.duplicate.prompt=書新戲之名 +instance.manage.duplicate.confirm=新戲將鈔例夾 (「.minecraft/versions/<例名>」) 之案,並另立行案、置設。 +instance.manage.manage=司例 +instance.manage.manage.title=司例 - %1s +instance.manage.redownload_assets_index=新戲之資案 +instance.manage.remove=除是例 +instance.manage.remove.confirm.trash=誠刪例「%s」乎?君猶可復之,蓋復案夾「%s」於棄案站也。 +instance.manage.remove.confirm.independent=以戲之用「(全例/例殊) 戲設 → 運徑 → 例獨」项,除之即咸除戲之檔與數矣!君其欲除之「%s」而後快邪? +instance.manage.remove_assets=刪戲之資案 +instance.manage.remove_libraries=率刪庫案 +instance.manage.rename=更名是例 +instance.manage.rename.message=告新名 +instance.manage.rename.fail=更名未成,或名有諱,或案見用。 +instance.search=名 +instance.search.prompt=書版名以尋 +instance.settings=戲設 +instance.update=更迭改囊集 + java.add=增爪哇 java.add.failed=爪哇謬抑弗兼此算機。 java.disable=禁此爪哇 @@ -1208,53 +1254,6 @@ update.tooltip=迭更 update.preview=先覽 HMCL 之版 update.preview.subtitle=啟此,君可先得 HMCL 之新,試之於未行。 -version=戲 -version.name=戲例之名 -version.cannot_read=不能閲戲例,無自裝矣 -version.empty=無游戲例 -version.empty.add=入引頁以裝戲 -version.empty.hint=無畢裝者。\n君且更他戲夾,抑擊斯以往戲之引頁。 -version.game.all=全覽 -version.game.april_fools=愚人節 -version.game.old=古典版 -version.game.release=當版 -version.game.releases=當版 -version.game.snapshot=預版 -version.game.snapshots=預版 -version.game.type=版類 -version.launch=戲 -version.launch.empty=啟戲 -version.launch.empty.installing=裝戲 -version.launch.empty.tooltip=裝啟至新之當版 -version.launch.test=試其戲 -version.switch=迭例 -version.launch_script=成啟戲角本 -version.launch_script.failed=弗能成啟戲角本 -version.launch_script.save=存啟戲角本 -version.launch_script.success=啟戲角本既成:%s -version.manage=例列 -version.manage.clean=掃戲之案夾 -version.manage.clean.tooltip=除空「logs」與「crash-reports」案夾 -version.manage.duplicate=鈔戲例 -version.manage.duplicate.duplicate_save=鈔生界 -version.manage.duplicate.prompt=書新戲之名 -version.manage.duplicate.confirm=新戲將鈔例夾 (「.minecraft/versions/<例名>」) 之案,並另立行案、置設。 -version.manage.manage=司例 -version.manage.manage.title=司例 - %1s -version.manage.redownload_assets_index=新戲之資案 -version.manage.remove=除是例 -version.manage.remove.confirm.trash=誠刪例「%s」乎?君猶可復之,蓋復案夾「%s」於棄案站也。 -version.manage.remove.confirm.independent=以戲之用「(全例/例殊) 戲設 → 運徑 → 例獨」项,除之即咸除戲之檔與數矣!君其欲除之「%s」而後快邪? -version.manage.remove_assets=刪戲之資案 -version.manage.remove_libraries=率刪庫案 -version.manage.rename=更名是例 -version.manage.rename.message=告新名 -version.manage.rename.fail=更名未成,或名有諱,或案見用。 -version.search=名 -version.search.prompt=書版名以尋 -version.settings=戲設 -version.update=更迭改囊集 - wiki.tooltip=礦藝大典之葉 wiki.version.game=https://lzh.minecraft.wiki/w/爪哇版%s wiki.version.game.snapshot=https://lzh.minecraft.wiki/w/%s diff --git a/HMCL/src/main/resources/assets/lang/I18N_ru.properties b/HMCL/src/main/resources/assets/lang/I18N_ru.properties index e9093c84542..b65431c3463 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_ru.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_ru.properties @@ -734,6 +734,52 @@ install.new_game.malformed=Недопустимое имя install.select=Выберите операцию install.success=Успешно установлено. +instance=Игры +instance.name=Название сборки +instance.empty=Нет сборок +instance.empty.add=Установить новую сборок +instance.empty.hint=Нет сборок.\nВы можете попробовать перейти в другую папку с игрой или нажать здесь, чтобы скачать игру. +instance.game.all=Все +instance.game.april_fools=День смеха +instance.game.old=Исторические +instance.game.release=Релиз +instance.game.releases=Релизы +instance.game.snapshot=Снапшот +instance.game.snapshots=Снапшоты +instance.game.type=Тип +instance.launch=Играть +instance.launch.empty=Играть +instance.launch.empty.installing=Установка игры +instance.launch.empty.tooltip=Установить и запустить последнюю официальную версию игры. +instance.launch.test=Тестовая игра +instance.switch=Сменить сборку +instance.launch_script=Создать сценарий запуска +instance.launch_script.failed=Невозможно создать сценарий запуска. +instance.launch_script.save=Сохранить сценарий запуска +instance.launch_script.success=Создан сценарий %s. +instance.manage=Все сборки +instance.manage.clean=Очистить журнал игры +instance.manage.clean.tooltip=Очистить файлы в папках «logs» и «crash-reports». +instance.manage.duplicate=Скопировать экземпляр сборки +instance.manage.duplicate.duplicate_save=Скопировать миры +instance.manage.duplicate.prompt=Введите новое название для этой сборки +instance.manage.duplicate.confirm=Скопированная сборка будет иметь копию всех файлов в папке сборки(«.minecraft/versions/<имя сборки>»), с изолированной рабочей папкой и настройками. +instance.manage.manage=Изменить сборку +instance.manage.manage.title=Изменить сборку - %1s +instance.manage.redownload_assets_index=Обновить файлы игровых активов +instance.manage.remove=Удалить +instance.manage.remove.confirm.trash=Удалить %s? Вы можете восстановить эту сборку с именем %s из корзины системы. +instance.manage.remove.confirm.independent=Поскольку эта сборка находится в режиме разделения, удаление этой сборки приведет к удалению всех миров, принадлежащих этой сборке. Удалить %s? +instance.manage.remove_assets=Удалить файлы игровых активов +instance.manage.remove_libraries=Удалить файлы библиотек +instance.manage.rename=Переименовать +instance.manage.rename.message=Введите новое название для этой сборки +instance.manage.rename.fail=Не удалось переименовать сборку. Возможно, некоторые файлы уже используются или в их имени содержится недопустимый символ. +instance.search=Название +instance.search.prompt=Введите название версии для поиска +instance.settings=Настройки +instance.update=Обновить модпак + java.add.failed=Этот Java недопустим или несовместим с текущей платформой. java.disable=Отключить Java java.disable.confirm=Вы уверены, что хотите отключить эту Java? @@ -1366,53 +1412,6 @@ update.latest=Это последняя версия. update.no_browser=Не удалось открыть браузер. Ссылка была скопирована в буфер обмена. Вставьте его в адресную строку браузера для обновления. update.tooltip=Обновить -version=Игры -version.name=Название сборки -version.cannot_read=Невозможно найти версию игры. Невозможно продолжить автоматическую установку. -version.empty=Нет сборок -version.empty.add=Установить новую сборок -version.empty.hint=Нет сборок.\nВы можете попробовать перейти в другую папку с игрой или нажать здесь, чтобы скачать игру. -version.game.all=Все -version.game.april_fools=День смеха -version.game.old=Исторические -version.game.release=Релиз -version.game.releases=Релизы -version.game.snapshot=Снапшот -version.game.snapshots=Снапшоты -version.game.type=Тип -version.launch=Играть -version.launch.empty=Играть -version.launch.empty.installing=Установка игры -version.launch.empty.tooltip=Установить и запустить последнюю официальную версию игры. -version.launch.test=Тестовая игра -version.switch=Сменить сборку -version.launch_script=Создать сценарий запуска -version.launch_script.failed=Невозможно создать сценарий запуска. -version.launch_script.save=Сохранить сценарий запуска -version.launch_script.success=Создан сценарий %s. -version.manage=Все сборки -version.manage.clean=Очистить журнал игры -version.manage.clean.tooltip=Очистить файлы в папках «logs» и «crash-reports». -version.manage.duplicate=Скопировать экземпляр сборки -version.manage.duplicate.duplicate_save=Скопировать миры -version.manage.duplicate.prompt=Введите новое название для этой сборки -version.manage.duplicate.confirm=Скопированная сборка будет иметь копию всех файлов в папке сборки(«.minecraft/versions/<имя сборки>»), с изолированной рабочей папкой и настройками. -version.manage.manage=Изменить сборку -version.manage.manage.title=Изменить сборку - %1s -version.manage.redownload_assets_index=Обновить файлы игровых активов -version.manage.remove=Удалить -version.manage.remove.confirm.trash=Удалить %s? Вы можете восстановить эту сборку с именем %s из корзины системы. -version.manage.remove.confirm.independent=Поскольку эта сборка находится в режиме разделения, удаление этой сборки приведет к удалению всех миров, принадлежащих этой сборке. Удалить %s? -version.manage.remove_assets=Удалить файлы игровых активов -version.manage.remove_libraries=Удалить файлы библиотек -version.manage.rename=Переименовать -version.manage.rename.message=Введите новое название для этой сборки -version.manage.rename.fail=Не удалось переименовать сборку. Возможно, некоторые файлы уже используются или в их имени содержится недопустимый символ. -version.search=Название -version.search.prompt=Введите название версии для поиска -version.settings=Настройки -version.update=Обновить модпак - wiki.tooltip=Страница Minecraft Wiki wiki.version.game=https://ru.minecraft.wiki/w/%s_(Java_Edition) wiki.version.game.snapshot=https://ru.minecraft.wiki/w/%s_(Java_Edition) diff --git a/HMCL/src/main/resources/assets/lang/I18N_uk.properties b/HMCL/src/main/resources/assets/lang/I18N_uk.properties index 3607e586ebe..1bc2362db95 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_uk.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_uk.properties @@ -685,6 +685,50 @@ install.new_game.malformed=Недійсна назва. install.select=Виберіть операцію install.success=Успішно встановлено. +instance=Ігри +instance.name=Назва екземпляра +instance.empty=Немає екземплярів +instance.empty.add=Додати новий екземпляр +instance.empty.hint=Тут немає екземплярів Minecraft. Ви можете спробувати переключитися на інший ігровий каталог або натиснути тут, щоб завантажити один. +instance.game.all=Усі +instance.game.april_fools=Перший квітня +instance.game.old=Історичні +instance.game.release=Реліз +instance.game.releases=Релізи +instance.game.snapshot=Снапшот +instance.game.snapshots=Снапшоти +instance.game.type=Тип +instance.launch=Запустити гру +instance.launch.empty=Почати гру +instance.launch.empty.installing=Встановлення гри +instance.launch.empty.tooltip=Встановити та запустити останній офіційний реліз +instance.launch.test=Тестовий запуск +instance.switch=Переключити екземпляр +instance.launch_script=Експортувати скрипт запуску +instance.launch_script.failed=Не вдалося експортувати скрипт запуску. +instance.launch_script.save=Експортувати скрипт запуску +instance.launch_script.success=Експортовано скрипт запуску як %s. +instance.manage=Усі екземпляри +instance.manage.clean=Видалити файли журналів +instance.manage.clean.tooltip=Видалити файли в каталогах "logs" та "crash-reports". +instance.manage.duplicate=Дублювати екземпляр +instance.manage.duplicate.prompt=Введіть нову назву екземпляра +instance.manage.duplicate.confirm=Дубльований екземпляр матиме копію всіх файлів у каталозі екземпляра (".minecraft/versions/<назва екземпляра>"), з ізольованим робочим каталогом та налаштуваннями. +instance.manage.manage=Редагувати екземпляр +instance.manage.manage.title=Редагувати екземпляр - %1s +instance.manage.redownload_assets_index=Оновити ресурси гри +instance.manage.remove=Видалити екземпляр +instance.manage.remove.confirm.trash=Ви впевнені, що хочете видалити екземпляр "%s"? Ви все ще можете знайти його файли у своєму кошику за назвою "%s". +instance.manage.remove_assets=Видалити всі ресурси +instance.manage.remove_libraries=Видалити всі бібліотеки +instance.manage.rename=Перейменувати екземпляр +instance.manage.rename.message=Введіть нову назву екземпляра +instance.manage.rename.fail=Не вдалося перейменувати екземпляр. Деякі файли можуть використовуватися або назва містить недійсний символ. +instance.search=Назва +instance.search.prompt=Введіть назву версії для пошуку +instance.settings=Налаштування +instance.update=Оновити модпак + java.add.failed=Ця Java недійсна або несумісна з поточною платформою. java.disable=Вимкнути Java java.disable.confirm=Ви впевнені, що хочете вимкнути цю Java? @@ -1319,51 +1363,6 @@ update.latest=Це остання версія update.no_browser=Не вдається відкрити в системному браузері. Але ми скопіювали посилання до вашого буфера обміну, і ви можете відкрити його вручну. update.tooltip=Оновити -version=Ігри -version.name=Назва екземпляра -version.cannot_read=Не вдалося розібрати екземпляр гри, встановлення не може продовжуватися. -version.empty=Немає екземплярів -version.empty.add=Додати новий екземпляр -version.empty.hint=Тут немає екземплярів Minecraft. Ви можете спробувати переключитися на інший ігровий каталог або натиснути тут, щоб завантажити один. -version.game.all=Усі -version.game.april_fools=Перший квітня -version.game.old=Історичні -version.game.release=Реліз -version.game.releases=Релізи -version.game.snapshot=Снапшот -version.game.snapshots=Снапшоти -version.game.type=Тип -version.launch=Запустити гру -version.launch.empty=Почати гру -version.launch.empty.installing=Встановлення гри -version.launch.empty.tooltip=Встановити та запустити останній офіційний реліз -version.launch.test=Тестовий запуск -version.switch=Переключити екземпляр -version.launch_script=Експортувати скрипт запуску -version.launch_script.failed=Не вдалося експортувати скрипт запуску. -version.launch_script.save=Експортувати скрипт запуску -version.launch_script.success=Експортовано скрипт запуску як %s. -version.manage=Усі екземпляри -version.manage.clean=Видалити файли журналів -version.manage.clean.tooltip=Видалити файли в каталогах "logs" та "crash-reports". -version.manage.duplicate=Дублювати екземпляр -version.manage.duplicate.prompt=Введіть нову назву екземпляра -version.manage.duplicate.confirm=Дубльований екземпляр матиме копію всіх файлів у каталозі екземпляра (".minecraft/versions/<назва екземпляра>"), з ізольованим робочим каталогом та налаштуваннями. -version.manage.manage=Редагувати екземпляр -version.manage.manage.title=Редагувати екземпляр - %1s -version.manage.redownload_assets_index=Оновити ресурси гри -version.manage.remove=Видалити екземпляр -version.manage.remove.confirm.trash=Ви впевнені, що хочете видалити екземпляр "%s"? Ви все ще можете знайти його файли у своєму кошику за назвою "%s". -version.manage.remove_assets=Видалити всі ресурси -version.manage.remove_libraries=Видалити всі бібліотеки -version.manage.rename=Перейменувати екземпляр -version.manage.rename.message=Введіть нову назву екземпляра -version.manage.rename.fail=Не вдалося перейменувати екземпляр. Деякі файли можуть використовуватися або назва містить недійсний символ. -version.search=Назва -version.search.prompt=Введіть назву версії для пошуку -version.settings=Налаштування -version.update=Оновити модпак - wiki.tooltip=Сторінка Minecraft Wiki wiki.version.game=https://uk.minecraft.wiki/w/%s_(Java_Edition) wiki.version.game.snapshot=https://uk.minecraft.wiki/w/%s_(Java_Edition) diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 69a467a554d..670265444f5 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -623,6 +623,58 @@ install.new_game.malformed_json=無效的 Minecraft 實例 JSON 文件。 install.select=請選取安裝方式 install.success=安裝成功 +instance=遊戲 +instance.name=遊戲實例名稱 +instance.empty=沒有遊戲實例 +instance.empty.add=進入下載頁安裝遊戲 +instance.empty.launch=沒有可啟動的遊戲。\n你可以前往「下載」頁面下載新遊戲,或在「實例清單」切換遊戲目錄。 +instance.empty.launch.goto_download=前往下載頁面 +instance.empty.hint=沒有已安裝的遊戲。\n你可以切換其他遊戲目錄,或者點擊此處進入遊戲下載頁面。 +instance.game.all=全部 +instance.game.april_fools=愚人節 +instance.game.old=遠古版 +instance.game.release=正式版 +instance.game.releases=正式版 +instance.game.snapshot=快照 +instance.game.snapshots=快照 +instance.game.support_status.unsupported=不支援 +instance.game.support_status.untested=未經測試 +instance.game.type=版本類型 +instance.launch=啟動遊戲 +instance.launch_and_enter_world=進入世界 +instance.launch.empty=開始遊戲 +instance.launch.empty.installing=安裝遊戲 +instance.launch.empty.tooltip=安裝並啟動最新正式版遊戲 +instance.launch.test=測試遊戲 +instance.switch=切換實例 +instance.launch_script=生成啟動指令碼 +instance.launch_script.failed=生成啟動指令碼失敗 +instance.launch_script.save=儲存啟動指令碼 +instance.launch_script.success=啟動指令碼已生成完畢: %s +instance.manage=實例清單 +instance.manage.clean=清理遊戲目錄 +instance.manage.clean.tooltip=清理「logs」與「crash-reports」目錄 +instance.manage.duplicate=複製遊戲實例 +instance.manage.duplicate.duplicate_save=複製世界 +instance.manage.duplicate.prompt=請輸入新遊戲實例名稱 +instance.manage.duplicate.confirm=新的遊戲將複製該實例目錄 (".minecraft/versions/<實例名>") 下的檔案,並帶有獨立的執行目錄和設定。 +instance.manage.manage=實例管理 +instance.manage.manage.title=實例管理 - %1s +instance.manage.redownload_assets_index=更新遊戲資源檔案 +instance.manage.remove=刪除該實例 +instance.manage.remove.confirm.trash=真的要刪除實例「%s」嗎? 你可以在系統的資源回收筒 (或垃圾桶) 中還原目錄「%s」來找回該實例。 +instance.manage.remove.confirm.independent=由於該實例啟用了「(全域/實例特定) 遊戲設定 → 執行路徑 → 各實例獨立」設定,刪除該實例將導致該遊戲的世界等資料一同被刪除!真的要刪除實例「%s」嗎? +instance.manage.remove.failed=刪除實例失敗。可能檔案被占用。 +instance.manage.remove_assets=刪除所有遊戲資源檔案 +instance.manage.remove_libraries=刪除所有支援庫檔案 +instance.manage.rename=重新命名該實例 +instance.manage.rename.message=請輸入新名稱 +instance.manage.rename.fail=重新命名實例失敗,可能檔案被佔用或者名稱有特殊字元。 +instance.search=名稱 +instance.search.prompt=輸入版本名稱進行搜尋 +instance.settings=遊戲設定 +instance.update=更新模組包 + java.add=新增 Java java.add.failed=Java 無效或與目前平臺不相容 java.disable=停用此 Java @@ -1587,59 +1639,6 @@ update.tooltip=更新 update.preview=提前測試 HMCL 預覽版本 update.preview.subtitle=啟用此選項,你將可以提前取得 HMCL 的新版本,以便在正式發布前進行測試。 -version=遊戲 -version.name=遊戲實例名稱 -version.cannot_read=讀取遊戲實例失敗,無法進行自動安裝 -version.empty=沒有遊戲實例 -version.empty.add=進入下載頁安裝遊戲 -version.empty.launch=沒有可啟動的遊戲。\n你可以前往「下載」頁面下載新遊戲,或在「實例清單」切換遊戲目錄。 -version.empty.launch.goto_download=前往下載頁面 -version.empty.hint=沒有已安裝的遊戲。\n你可以切換其他遊戲目錄,或者點擊此處進入遊戲下載頁面。 -version.game.all=全部 -version.game.april_fools=愚人節 -version.game.old=遠古版 -version.game.release=正式版 -version.game.releases=正式版 -version.game.snapshot=快照 -version.game.snapshots=快照 -version.game.support_status.unsupported=不支援 -version.game.support_status.untested=未經測試 -version.game.type=版本類型 -version.launch=啟動遊戲 -version.launch_and_enter_world=進入世界 -version.launch.empty=開始遊戲 -version.launch.empty.installing=安裝遊戲 -version.launch.empty.tooltip=安裝並啟動最新正式版遊戲 -version.launch.test=測試遊戲 -version.switch=切換實例 -version.launch_script=生成啟動指令碼 -version.launch_script.failed=生成啟動指令碼失敗 -version.launch_script.save=儲存啟動指令碼 -version.launch_script.success=啟動指令碼已生成完畢: %s -version.manage=實例清單 -version.manage.clean=清理遊戲目錄 -version.manage.clean.tooltip=清理「logs」與「crash-reports」目錄 -version.manage.duplicate=複製遊戲實例 -version.manage.duplicate.duplicate_save=複製世界 -version.manage.duplicate.prompt=請輸入新遊戲實例名稱 -version.manage.duplicate.confirm=新的遊戲將複製該實例目錄 (".minecraft/versions/<實例名>") 下的檔案,並帶有獨立的執行目錄和設定。 -version.manage.manage=實例管理 -version.manage.manage.title=實例管理 - %1s -version.manage.redownload_assets_index=更新遊戲資源檔案 -version.manage.remove=刪除該實例 -version.manage.remove.confirm.trash=真的要刪除實例「%s」嗎? 你可以在系統的資源回收筒 (或垃圾桶) 中還原目錄「%s」來找回該實例。 -version.manage.remove.confirm.independent=由於該實例啟用了「(全域/實例特定) 遊戲設定 → 執行路徑 → 各實例獨立」設定,刪除該實例將導致該遊戲的世界等資料一同被刪除!真的要刪除實例「%s」嗎? -version.manage.remove.failed=刪除實例失敗。可能檔案被占用。 -version.manage.remove_assets=刪除所有遊戲資源檔案 -version.manage.remove_libraries=刪除所有支援庫檔案 -version.manage.rename=重新命名該實例 -version.manage.rename.message=請輸入新名稱 -version.manage.rename.fail=重新命名實例失敗,可能檔案被佔用或者名稱有特殊字元。 -version.search=名稱 -version.search.prompt=輸入版本名稱進行搜尋 -version.settings=遊戲設定 -version.update=更新模組包 - warning.java_interpreted_mode=HMCL 正在執行於直譯器模式的 Java 環境中,效能將會受到很大影響。\n我們建議你使用支援 JIT 的 Java 開啟 HMCL 以獲得最佳體驗。 warning.software_rendering=HMCL 正在使用軟體繪製,效能將會受到很大影響。 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index bdf7c024f44..fefa4a8469e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -628,6 +628,58 @@ install.new_game.malformed_json=不是有效的 Minecraft 实例 JSON 文件。 install.select=请选择安装方式 install.success=安装成功 +instance=游戏 +instance.name=游戏实例名称 +instance.empty=没有游戏实例 +instance.empty.add=进入下载页安装游戏 +instance.empty.launch=没有可启动的游戏。\n你可以前往下载页面下载新游戏,或在“实例列表”中切换游戏文件夹。 +instance.empty.launch.goto_download=前往下载页面 +instance.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹,或者点击此处进入游戏下载页面。 +instance.game.all=全部 +instance.game.april_fools=愚人节 +instance.game.old=远古版 +instance.game.release=正式版 +instance.game.releases=正式版 +instance.game.snapshot=快照 +instance.game.snapshots=快照 +instance.game.support_status.unsupported=不支持 +instance.game.support_status.untested=未经测试 +instance.game.type=版本类型 +instance.launch=启动游戏 +instance.launch_and_enter_world=进入世界 +instance.launch.empty=开始游戏 +instance.launch.empty.installing=安装游戏 +instance.launch.empty.tooltip=安装并启动最新正式版游戏 +instance.launch.test=测试游戏 +instance.switch=切换实例 +instance.launch_script=生成启动脚本 +instance.launch_script.failed=生成启动脚本失败 +instance.launch_script.save=保存启动脚本 +instance.launch_script.success=启动脚本已生成完毕:%s +instance.manage=实例列表 +instance.manage.clean=清理游戏文件夹 +instance.manage.clean.tooltip=清理“logs”和“crash-reports”文件夹 +instance.manage.duplicate=复制游戏实例 +instance.manage.duplicate.duplicate_save=复制世界 +instance.manage.duplicate.prompt=请输入新游戏名称 +instance.manage.duplicate.confirm=新的游戏实例将复制该实例文件夹 (".minecraft/versions/<实例名>") 下的文件,并带有独立的运行文件夹和设置。 +instance.manage.manage=实例管理 +instance.manage.manage.title=实例管理 - %1s +instance.manage.redownload_assets_index=更新游戏资源文件 +instance.manage.remove=删除该实例 +instance.manage.remove.confirm.trash=真的要删除实例“%s”吗?你可以在系统的回收站中还原“%s”文件夹来找回该实例。 +instance.manage.remove.confirm.independent=由于该游戏启用了“(全局/实例特定) 游戏设置 → 版本隔离 → 各实例独立”选项,删除该实例将导致该游戏的世界等数据一同被删除!真的要删除实例“%s”吗? +instance.manage.remove.failed=删除实例失败。可能文件被占用。 +instance.manage.remove_assets=删除所有游戏资源文件 +instance.manage.remove_libraries=删除所有库文件 +instance.manage.rename=重命名该实例 +instance.manage.rename.message=请输入要修改的名称 +instance.manage.rename.fail=重命名实例失败,可能文件被占用或者名字有特殊字符。 +instance.search=名称 +instance.search.prompt=输入版本名称进行搜索 +instance.settings=游戏设置 +instance.update=更新整合包 + java.add=添加 Java java.add.failed=Java 无效或与当前平台不兼容。 java.disable=禁用此 Java @@ -1592,59 +1644,6 @@ update.tooltip=更新 update.preview=提前预览 HMCL 版本 update.preview.subtitle=启用此选项,你将可以提前获取 HMCL 的新版本,以便在正式发布前进行测试。 -version=游戏 -version.name=游戏实例名称 -version.cannot_read=读取游戏实例失败,无法进行自动安装 -version.empty=没有游戏实例 -version.empty.add=进入下载页安装游戏 -version.empty.launch=没有可启动的游戏。\n你可以前往下载页面下载新游戏,或在“实例列表”中切换游戏文件夹。 -version.empty.launch.goto_download=前往下载页面 -version.empty.hint=没有已安装的游戏。\n你可以切换其他游戏文件夹,或者点击此处进入游戏下载页面。 -version.game.all=全部 -version.game.april_fools=愚人节 -version.game.old=远古版 -version.game.release=正式版 -version.game.releases=正式版 -version.game.snapshot=快照 -version.game.snapshots=快照 -version.game.support_status.unsupported=不支持 -version.game.support_status.untested=未经测试 -version.game.type=版本类型 -version.launch=启动游戏 -version.launch_and_enter_world=进入世界 -version.launch.empty=开始游戏 -version.launch.empty.installing=安装游戏 -version.launch.empty.tooltip=安装并启动最新正式版游戏 -version.launch.test=测试游戏 -version.switch=切换实例 -version.launch_script=生成启动脚本 -version.launch_script.failed=生成启动脚本失败 -version.launch_script.save=保存启动脚本 -version.launch_script.success=启动脚本已生成完毕:%s -version.manage=实例列表 -version.manage.clean=清理游戏文件夹 -version.manage.clean.tooltip=清理“logs”和“crash-reports”文件夹 -version.manage.duplicate=复制游戏实例 -version.manage.duplicate.duplicate_save=复制世界 -version.manage.duplicate.prompt=请输入新游戏名称 -version.manage.duplicate.confirm=新的游戏实例将复制该实例文件夹 (".minecraft/versions/<实例名>") 下的文件,并带有独立的运行文件夹和设置。 -version.manage.manage=实例管理 -version.manage.manage.title=实例管理 - %1s -version.manage.redownload_assets_index=更新游戏资源文件 -version.manage.remove=删除该实例 -version.manage.remove.confirm.trash=真的要删除实例“%s”吗?你可以在系统的回收站中还原“%s”文件夹来找回该实例。 -version.manage.remove.confirm.independent=由于该游戏启用了“(全局/实例特定) 游戏设置 → 版本隔离 → 各实例独立”选项,删除该实例将导致该游戏的世界等数据一同被删除!真的要删除实例“%s”吗? -version.manage.remove.failed=删除实例失败。可能文件被占用。 -version.manage.remove_assets=删除所有游戏资源文件 -version.manage.remove_libraries=删除所有库文件 -version.manage.rename=重命名该实例 -version.manage.rename.message=请输入要修改的名称 -version.manage.rename.fail=重命名实例失败,可能文件被占用或者名字有特殊字符。 -version.search=名称 -version.search.prompt=输入版本名称进行搜索 -version.settings=游戏设置 -version.update=更新整合包 - warning.java_interpreted_mode=HMCL 正在运行在解释器模式的 Java 环境中,性能将会受到很大影响。\n我们建议你使用支持 JIT 的 Java 启动 HMCL 以获取最佳体验。 warning.software_rendering=HMCL 正在使用软件渲染,性能将会受到很大影响。\n你可以访问 https://docs.hmcl.net/help.html 页面寻求帮助。 diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java index 3a1977d00c5..cd41d0a9014 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/setting/GameDirectoriesTest.java @@ -25,6 +25,7 @@ import javafx.beans.property.ObjectProperty; import javafx.collections.ObservableList; import org.jackhuang.hmcl.Metadata; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.HMCLGameRepository; import org.jackhuang.hmcl.util.FileSaver; import org.jackhuang.hmcl.util.PortablePath; @@ -140,7 +141,7 @@ public void migratesLegacySelectedVersionsFromConfigurations() { assertFalse(settings.has("configurations")); assertEquals(id, config.selectedGameDirectoryProperty().get()); - assertEquals("1.20.1", config.getSelectedInstance(id)); + assertEquals("1.20.1", config.getSelectedInstance(id).toString()); JsonObject serialized = JsonParser.parseString(config.toJson()).getAsJsonObject(); assertEquals("1.20.1", serialized @@ -465,7 +466,8 @@ public void legacyInstanceSettingsMigrationStoresLegacyGameDirectoryPresetAsPare new GameDirectoryEnvironment(localDirectories, userDirectories, presets)) { settings().defaultGameSettingsPresetProperty().set(defaultPresetId); HMCLGameRepository repository = new HMCLGameRepository(gameDirectory); - Path versionRoot = repository.getVersionRoot("1.20.1"); + GameInstanceID instanceId = new GameInstanceID("1.20.1"); + Path versionRoot = repository.getInstanceRoot(instanceId); Files.createDirectories(versionRoot); Files.writeString(versionRoot.resolve("hmclversion.cfg"), """ { @@ -473,7 +475,7 @@ public void legacyInstanceSettingsMigrationStoresLegacyGameDirectoryPresetAsPare } """); - GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1")); + GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings(instanceId)); assertEquals(legacyPresetId, setting.parentProperty().getValue()); } @@ -506,7 +508,8 @@ public void startupMigrationSkipsLegacyInstanceSettingsFile(@TempDir Path tempDi settings().defaultGameSettingsPresetProperty().set(defaultPresetId); HMCLGameRepository repository = new HMCLGameRepository(gameDirectory); writeVersionJson(repository, "1.20.1"); - Path versionRoot = repository.getVersionRoot("1.20.1"); + GameInstanceID instanceId = new GameInstanceID("1.20.1"); + Path versionRoot = repository.getInstanceRoot(instanceId); Files.writeString(versionRoot.resolve(LegacyGameSettingsMigrator.LEGACY_INSTANCE_SETTINGS_FILENAME), """ { "usesGlobal": true @@ -515,9 +518,9 @@ public void startupMigrationSkipsLegacyInstanceSettingsFile(@TempDir Path tempDi LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets); - assertFalse(Files.exists(repository.getInstanceConfigDirectory("1.20.1") + assertFalse(Files.exists(repository.getInstanceConfigDirectory(instanceId) .resolve(LegacyGameSettingsMigrator.INSTANCE_GAME_SETTINGS_FILENAME))); - GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1")); + GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings(instanceId)); assertEquals(legacyPresetId, setting.parentProperty().getValue()); } } @@ -553,7 +556,7 @@ public void absentInstanceSettingsStoreLegacyGameDirectoryPresetAsParent(@TempDi writeVersionJson(repository, "1.20.1"); LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets); - GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings("1.20.1")); + GameSettings.Instance setting = Objects.requireNonNull(repository.getInstanceGameSettings(new GameInstanceID("1.20.1"))); assertEquals(legacyPresetId, setting.parentProperty().getValue()); assertTrue(setting.getOverrideProperties().contains(GameSettings.PROPERTY_RUNNING_DIRECTORY)); @@ -591,7 +594,7 @@ public void newInstanceAfterMigrationDoesNotUseLegacyGameDirectoryParent(@TempDi LegacyConfigMigrator.migrateLegacyInstanceGameSettings(localDirectories, presets); writeVersionJson(repository, "1.20.1"); - assertNull(repository.getInstanceGameSettings("1.20.1")); + assertNull(repository.getInstanceGameSettings(new GameInstanceID("1.20.1"))); } } @@ -774,7 +777,7 @@ public void close() throws ReflectiveOperationException { /// Writes a minimal valid version json for repository refresh tests. private static void writeVersionJson(HMCLGameRepository repository, String id) throws IOException { - Path versionRoot = repository.getVersionRoot(id); + Path versionRoot = repository.getInstanceRoot(new GameInstanceID(id)); Files.createDirectories(versionRoot); Files.writeString(versionRoot.resolve(id + ".json"), """ { diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/setting/TypedIDTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/setting/TypedIDTest.java index 2c944171a9c..3644b879cbc 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/setting/TypedIDTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/setting/TypedIDTest.java @@ -20,6 +20,7 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.jackhuang.hmcl.auth.AccountID; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jetbrains.annotations.NotNullByDefault; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -100,7 +101,7 @@ public void serializesLauncherSettingsMapKeysAsPrefixedStrings() { GameDirectoryID id = GameDirectoryID.parse("game-directory:123e4567-e89b-12d3-a456-426614174000"); LauncherSettings settings = new LauncherSettings(); - settings.setSelectedInstance(id, "1.20.1"); + settings.setSelectedInstance(id, new GameInstanceID("1.20.1")); JsonObject serialized = JsonParser.parseString(settings.toJson()).getAsJsonObject(); LauncherSettings deserialized = Objects.requireNonNull(LauncherSettings.fromJson(serialized)); @@ -108,6 +109,6 @@ public void serializesLauncherSettingsMapKeysAsPrefixedStrings() { .getAsJsonObject(LauncherSettings.PROPERTY_SELECTED_INSTANCE) .get(id.toString()) .getAsString()); - assertEquals("1.20.1", deserialized.getSelectedInstance(id)); + assertEquals("1.20.1", deserialized.getSelectedInstance(id).toString()); } } diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java index 248e027c07a..0fb1e46b021 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/ui/GameCrashWindowTest.java @@ -18,7 +18,8 @@ package org.jackhuang.hmcl.ui; import org.jackhuang.hmcl.JavaFXLauncher; -import org.jackhuang.hmcl.game.ClassicVersion; +import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.LaunchOptions; import org.jackhuang.hmcl.java.JavaInfo; import org.jackhuang.hmcl.game.Log; @@ -52,7 +53,7 @@ public void test() throws Exception { Path workingPath = Path.of(System.getProperty("user.dir")); GameCrashWindow window = new GameCrashWindow(process, ProcessListener.ExitType.APPLICATION_ERROR, null, - new ClassicVersion(), + new GameInstanceManifest(new GameInstanceID("Classic")), new LaunchOptions.Builder() .setJava(new JavaRuntime(workingPath, new JavaInfo(Platform.SYSTEM_PLATFORM, "16", null), false, false)) .setGameDir(workingPath) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java index 0520b67a8fc..0946df626fb 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/LocalAddonManager.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.addon; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.FileUtils; @@ -47,19 +48,19 @@ public static String getLocalAddonName(Path file) { protected final Set<@NotNull T> localFiles = new LinkedHashSet<>(); protected final GameRepository repository; - protected final String id; + protected final GameInstanceID instanceId; - public LocalAddonManager(GameRepository gameRepository, String versionId) { + public LocalAddonManager(GameRepository gameRepository, GameInstanceID instanceId) { this.repository = gameRepository; - this.id = versionId; + this.instanceId = instanceId; } public GameRepository getRepository() { return repository; } - public String getInstanceId() { - return id; + public GameInstanceID getInstanceId() { + return instanceId; } public abstract Path getDirectory(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java index 8acc3151117..ab60fac8b29 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/mod/ModManager.java @@ -22,7 +22,9 @@ import org.jackhuang.hmcl.addon.LocalAddonManager; import org.jackhuang.hmcl.addon.meta.*; import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.GameRepository; +import org.jackhuang.hmcl.game.NoSuchGameInstanceException; import org.jackhuang.hmcl.util.Pair; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.io.CompressingUtils; @@ -68,13 +70,13 @@ private interface ModMetadataReader { private boolean loaded = false; - public ModManager(GameRepository repository, String id) { + public ModManager(GameRepository repository, GameInstanceID id) { super(repository, id); } @Override public Path getDirectory() { - return repository.getModsDirectory(id); + return repository.getModsDirectory(instanceId); } public LibraryAnalyzer getLibraryAnalyzer() { @@ -178,7 +180,11 @@ public void refresh() throws IOException { localFiles.clear(); localMods.clear(); - analyzer = LibraryAnalyzer.analyze(getRepository().getResolvedPreservingPatchesVersion(id), null); + try { + analyzer = LibraryAnalyzer.analyze(getRepository().getResolvedInstanceManifest(instanceId), null); + } catch (NoSuchGameInstanceException e) { + throw new IOException(e); + } boolean supportSubfolders = analyzer.has(LibraryAnalyzer.LibraryType.FORGE) || analyzer.has(LibraryAnalyzer.LibraryType.QUILT); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java index d571f6422cf..8c1e93d0dc3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/addon/resourcepack/ResourcePackManager.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.addon.resourcepack; import com.google.gson.annotations.SerializedName; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.addon.LocalAddonManager; import org.jackhuang.hmcl.addon.meta.PackMcMeta; @@ -218,10 +219,10 @@ private static List deserializePackList(String json) { private boolean loaded = false; - public ResourcePackManager(GameRepository repository, String id) { - super(repository, id); - this.resourcePackDirectory = this.repository.getResourcePackDirectory(this.id); - this.optionsFile = repository.getRunDirectory(id).resolve("options.txt"); + public ResourcePackManager(GameRepository repository, GameInstanceID instanceId) { + super(repository, instanceId); + this.resourcePackDirectory = this.repository.getResourcePackDirectory(this.instanceId); + this.optionsFile = repository.getRunDirectory(instanceId).resolve("options.txt"); } @NotNull @@ -262,7 +263,7 @@ public GameVersionNumber getMinecraftVersion() { lock.lock(); try { if (minecraftVersion == null) { - minecraftVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(id)); + minecraftVersion = GameVersionNumber.asGameVersion(repository.getGameVersion(instanceId)); supportsNewOptionsFormat = isMcVersionSupportsNewOptionsFormat(minecraftVersion); } } finally { @@ -277,7 +278,7 @@ public PackMcMeta.PackVersion getRequiredVersion() { if (requiredVersion == null) { lock.lock(); try { - if (requiredVersion == null) requiredVersion = getPackVersion(getMinecraftVersion(), repository.getVersionJar(id)); + if (requiredVersion == null) requiredVersion = getPackVersion(getMinecraftVersion(), repository.getInstanceJar(instanceId)); } finally { lock.unlock(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java index 455aa622e00..89d4264419d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultDependencyManager.java @@ -26,8 +26,8 @@ import org.jackhuang.hmcl.download.optifine.OptiFineInstallTask; import org.jackhuang.hmcl.game.Artifact; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.FileUtils; @@ -73,43 +73,43 @@ public DefaultCacheRepository getCacheRepository() { } @Override - public GameBuilder gameBuilder() { + public GameBuilder newGameBuilder() { return new DefaultGameBuilder(this); } @Override - public Task checkGameCompletionAsync(Version version, boolean integrityCheck) { + public Task checkGameCompletionAsync(GameInstanceManifest manifest, boolean integrityCheck) { return Task.allOf( Task.composeAsync(() -> { - Path versionJar = repository.getVersionJar(version); + Path versionJar = repository.getInstanceJar(manifest); return Files.notExists(versionJar) || FileUtils.size(versionJar) == 0L - ? new GameDownloadTask(this, null, version) + ? new GameDownloadTask(this, null, manifest) : null; - }).thenComposeAsync(checkPatchCompletionAsync(version, integrityCheck)), - new GameAssetDownloadTask(this, version, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY, integrityCheck) + }).thenComposeAsync(checkPatchCompletionAsync(manifest, integrityCheck)), + new GameAssetDownloadTask(this, manifest, GameAssetDownloadTask.DOWNLOAD_INDEX_IF_NECESSARY, integrityCheck) .setSignificance(Task.TaskSignificance.MODERATE), - new GameLibrariesTask(this, version, integrityCheck) + new GameLibrariesTask(this, manifest, integrityCheck) ); } @Override - public Task checkLibraryCompletionAsync(Version version, boolean integrityCheck) { - return new GameLibrariesTask(this, version, integrityCheck, version.getLibraries()); + public Task checkLibraryCompletionAsync(GameInstanceManifest manifest, boolean integrityCheck) { + return new GameLibrariesTask(this, manifest, integrityCheck, manifest.getLibraries()); } @Override - public Task checkPatchCompletionAsync(Version version, boolean integrityCheck) { + public Task checkPatchCompletionAsync(GameInstanceManifest manifest, boolean integrityCheck) { return Task.composeAsync(() -> { List> tasks = new ArrayList<>(0); - String gameVersion = repository.getGameVersion(version).orElse(null); + String gameVersion = repository.getGameVersion(manifest).orElse(null); if (gameVersion == null) return null; - Version original = repository.getVersion(version.getId()); - Version resolved = original.resolvePreservingPatches(repository); + GameInstanceManifest original = repository.getInstanceManifest(manifest.id()); + GameInstanceManifest.Resolved resolvedInstanceManifest = repository.getResolvedInstanceManifest(manifest.id()); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolved, gameVersion); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(resolvedInstanceManifest, gameVersion); for (LibraryAnalyzer.LibraryType type : LibraryAnalyzer.LibraryType.values()) { if (!analyzer.has(type)) continue; @@ -120,23 +120,23 @@ public Task checkPatchCompletionAsync(Version version, boolean integrityCheck Matcher matcher = Pattern.compile("^([0-9.]+)_(?HD_.+)$").matcher(optifineVersion); return matcher.find() ? matcher.group("optifine") : optifineVersion; }) - .orElseGet(() -> resolved.getPatches().stream() - .filter(patch -> "optifine".equals(patch.getId())) + .orElseGet(() -> resolvedInstanceManifest.standaloneManifest().getPatches().stream() + .filter(patch -> "optifine".equals(patch.id())) .findAny() - .map(Version::getVersion) + .map(gameInstancePatch -> gameInstancePatch.version()) .orElse(null)); - boolean needsReInstallation = version.getLibraries().stream() + boolean needsReInstallation = manifest.getLibraries().stream() .anyMatch(library -> !library.hasDownloadURL() && "optifine".equals(library.getGroupId()) - && GameLibrariesTask.shouldDownloadLibrary(repository, version, library, integrityCheck)); + && GameLibrariesTask.shouldDownloadLibrary(repository, manifest, library, integrityCheck)); if (needsReInstallation) { Library installer = new Library(new Artifact("optifine", "OptiFine", gameVersion + "_" + optifinePatchVersion, "installer")); - if (GameLibrariesTask.shouldDownloadLibrary(repository, version, installer, integrityCheck)) { + if (GameLibrariesTask.shouldDownloadLibrary(repository, manifest, installer, integrityCheck)) { tasks.add(installLibraryAsync(gameVersion, original, "optifine", optifinePatchVersion)); } else { - tasks.add(OptiFineInstallTask.install(this, original, repository.getLibraryFile(version, installer))); + tasks.add(OptiFineInstallTask.install(this, original, repository.getLibraryFile(manifest, installer))); } } } @@ -147,9 +147,7 @@ public Task checkPatchCompletionAsync(Version version, boolean integrityCheck } @Override - public Task installLibraryAsync(String gameVersion, Version baseVersion, String libraryId, String libraryVersion) { - if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved"); - + public Task installLibraryAsync(String gameVersion, GameInstanceManifest baseVersion, String libraryId, String libraryVersion) { VersionList versionList = getVersionList(libraryId); return versionList.loadAsync(gameVersion) .thenComposeAsync(() -> installLibraryAsync(baseVersion, versionList.getVersion(gameVersion, libraryVersion) @@ -158,12 +156,10 @@ public Task installLibraryAsync(String gameVersion, Version baseVersion } @Override - public Task installLibraryAsync(Version baseVersion, RemoteVersion libraryVersion) { - if (baseVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved"); - - AtomicReference removedLibraryVersion = new AtomicReference<>(); + public Task installLibraryAsync(GameInstanceManifest baseVersion, RemoteVersion libraryVersion) { + AtomicReference removedLibraryVersion = new AtomicReference<>(); - return removeLibraryAsync(baseVersion.resolvePreservingPatches(repository), libraryVersion.getLibraryId()) + return removeLibraryAsync(baseVersion, libraryVersion.getLibraryId()) .thenComposeAsync(version -> { removedLibraryVersion.set(version); return libraryVersion.getInstallTask(this, version); @@ -178,9 +174,7 @@ public Task installLibraryAsync(Version baseVersion, RemoteVersion libr .withStage(String.format("hmcl.install.%s:%s", libraryVersion.getLibraryId(), libraryVersion.getSelfVersion())); } - public Task installLibraryAsync(Version oldVersion, Path installer) { - if (oldVersion.isResolved()) throw new IllegalArgumentException("Version should not be resolved"); - + public Task installLibraryAsync(GameInstanceManifest oldVersion, Path installer) { return Task .composeAsync(() -> { try { @@ -205,7 +199,7 @@ public Task installLibraryAsync(Version oldVersion, Path installer) { throw new UnsupportedLibraryInstallerException(); }) - .thenApplyAsync(oldVersion::addPatch); + .thenApplyAsync(patch -> patch == null ? oldVersion : oldVersion.addPatch(patch)); } public static class UnsupportedLibraryInstallerException extends Exception { @@ -215,20 +209,19 @@ public static class UnsupportedLibraryInstallerException extends Exception { * Remove installed library. * Will try to remove libraries and patches. * - * @param version not resolved version + * @param manifest not resolved instance manifest * @param libraryId forge/liteloader/optifine/fabric * @return task to remove the specified library */ - public Task removeLibraryAsync(Version version, String libraryId) { + public Task removeLibraryAsync(GameInstanceManifest manifest, String libraryId) { // MaintainTask requires version that does not inherits from any version. // If we want to remove a library in dependent version, we should keep the dependents not changed // So resolving this game version to preserve all information in this version.json is necessary. - if (version.isResolved()) - throw new IllegalArgumentException("removeLibraryWithoutSavingAsync requires non-resolved version"); - Version independentVersion = version.resolvePreservingPatches(repository); - String gameVersion = repository.getGameVersion(independentVersion).orElse(null); - - return Task.supplyAsync(() -> LibraryAnalyzer.analyze(independentVersion, gameVersion).removeLibrary(libraryId).build()); + return Task.supplyAsync(() -> { + GameInstanceManifest independentVersion = repository.resolve(manifest).standaloneManifest(); + String gameVersion = repository.getGameVersion(independentVersion).orElse(null); + return LibraryAnalyzer.analyze(independentVersion, gameVersion).removeLibrary(libraryId).build(); + }); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java index b6966503ead..e1ac56b747b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DefaultGameBuilder.java @@ -17,7 +17,7 @@ */ package org.jackhuang.hmcl.download; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.function.ExceptionalFunction; @@ -44,7 +44,7 @@ public DefaultDependencyManager getDependencyManager() { public Task buildAsync() { var hints = new ArrayList(); - Task libraryTask = Task.supplyAsync(() -> new Version(name)); + Task libraryTask = Task.supplyAsync(() -> new GameInstanceManifest(name)); libraryTask = libraryTask.thenComposeAsync(libraryTaskHelper(gameVersion, "game", gameVersion)); hints.add(new Task.StagesHint("hmcl.install.game:" + gameVersion)); hints.add(new Task.StagesHint("hmcl.install.libraries")); @@ -62,11 +62,11 @@ public Task buildAsync() { return libraryTask.thenComposeAsync(dependencyManager.getGameRepository()::saveAsync).whenComplete(exception -> { if (exception != null) - dependencyManager.getGameRepository().removeVersionFromDisk(name); + dependencyManager.getGameRepository().removeInstanceFromDisk(name); }).withStagesHints(hints); } - private ExceptionalFunction, ?> libraryTaskHelper(String gameVersion, String libraryId, String libraryVersion) { + private ExceptionalFunction, ?> libraryTaskHelper(String gameVersion, String libraryId, String libraryVersion) { return version -> dependencyManager.installLibraryAsync(gameVersion, version, libraryId, libraryVersion); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java index 98501fc8cc6..16a6f43263d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/DependencyManager.java @@ -17,8 +17,8 @@ */ package org.jackhuang.hmcl.download; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameRepository; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.CacheRepository; @@ -46,7 +46,7 @@ public interface DependencyManager { * * @return the task to check game completion. */ - Task checkGameCompletionAsync(Version version, boolean integrityCheck); + Task checkGameCompletionAsync(GameInstanceManifest manifest, boolean integrityCheck); /** * Check if libraries of this version in complete. @@ -54,22 +54,22 @@ public interface DependencyManager { * * @return the task to check game completion. */ - Task checkLibraryCompletionAsync(Version version, boolean integrityCheck); + Task checkLibraryCompletionAsync(GameInstanceManifest manifest, boolean integrityCheck); /** * Check if patches of this version in complete. * If not, reinstall the patch if possible. * - * @param version the version to be checked + * @param manifest the version to be checked * @param integrityCheck check if some libraries are corrupt. * @return the task to check patches completion. */ - Task checkPatchCompletionAsync(Version version, boolean integrityCheck); + Task checkPatchCompletionAsync(GameInstanceManifest manifest, boolean integrityCheck); /** * The builder to build a brand new game then libraries such as Forge, LiteLoader and OptiFine. */ - GameBuilder gameBuilder(); + GameBuilder newGameBuilder(); /** * Install a library to a version. @@ -81,7 +81,7 @@ public interface DependencyManager { * @param libraryVersion the version of being installed library. * @return the task to install the specific library. */ - Task installLibraryAsync(String gameVersion, Version baseVersion, String libraryId, String libraryVersion); + Task installLibraryAsync(String gameVersion, GameInstanceManifest baseVersion, String libraryId, String libraryVersion); /** * Install a library to a version. @@ -91,7 +91,7 @@ public interface DependencyManager { * @param libraryVersion the remote version of being installed library. * @return the task to install the specific library. */ - Task installLibraryAsync(Version baseVersion, RemoteVersion libraryVersion); + Task installLibraryAsync(GameInstanceManifest baseVersion, RemoteVersion libraryVersion); /** * Get registered version list. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java index 4d2e3f5fdc7..c16c19d6003 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/GameBuilder.java @@ -17,7 +17,9 @@ */ package org.jackhuang.hmcl.download; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.task.Task; +import org.jetbrains.annotations.Nullable; import java.util.*; @@ -28,12 +30,12 @@ */ public abstract class GameBuilder { - protected String name = ""; + protected @Nullable GameInstanceID name; protected String gameVersion = ""; protected final Map toolVersions = new HashMap<>(); protected final Set remoteVersions = new HashSet<>(); - public String getName() { + public GameInstanceID getName() { return name; } @@ -42,7 +44,7 @@ public String getName() { * * @param name the name of new game version. */ - public GameBuilder name(String name) { + public GameBuilder name(GameInstanceID name) { this.name = Objects.requireNonNull(name); return this; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java index 90f7efd227a..a11e51ba5e4 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/LibraryAnalyzer.java @@ -34,11 +34,11 @@ import static org.jackhuang.hmcl.util.Pair.pair; public final class LibraryAnalyzer implements Iterable { - private Version version; + private GameInstanceManifest manifest; private final Map> libraries; - private LibraryAnalyzer(Version version, Map> libraries) { - this.version = version; + private LibraryAnalyzer(GameInstanceManifest manifest, Map> libraries) { + this.manifest = manifest; this.libraries = libraries; } @@ -60,7 +60,7 @@ public Optional getLibrary(LibraryType type) { * Maybe a guessing implementation will be provided in the future. But by now, we simply set it to JUST_EXISTED. */ public LibraryMark.LibraryStatus getLibraryStatus(String type) { - return version.hasPatch(type) ? LibraryMark.LibraryStatus.CLEAR : LibraryMark.LibraryStatus.JUST_EXISTED; + return manifest.hasPatch(type) ? LibraryMark.LibraryStatus.CLEAR : LibraryMark.LibraryStatus.JUST_EXISTED; } @NotNull @@ -97,17 +97,17 @@ public boolean hasModLoader() { } public boolean hasModLauncher() { - return LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(version.getMainClass()) || version.getPatches().stream().anyMatch( - patch -> LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(patch.getMainClass()) + return LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(manifest.mainClass()) || manifest.getPatches().stream().anyMatch( + patch -> LibraryAnalyzer.MOD_LAUNCHER_MAIN.equals(patch.mainClass()) ); } - private Version removingMatchedLibrary(Version version, String libraryId) { + private GameInstanceManifest removingMatchedLibrary(GameInstanceManifest manifest, String libraryId) { LibraryType type = LibraryType.fromPatchId(libraryId); - if (type == null) return version; + if (type == null) return manifest; List libraries = new ArrayList<>(); - List rawLibraries = version.getLibraries(); + List rawLibraries = manifest.getLibraries(); for (Library library : rawLibraries) { if (type.matchLibrary(library, rawLibraries)) { // skip @@ -115,7 +115,23 @@ private Version removingMatchedLibrary(Version version, String libraryId) { libraries.add(library); } } - return version.setLibraries(libraries); + return manifest.withLibraries(libraries); + } + + private GameInstancePatch removingMatchedLibrary(GameInstancePatch patch, String libraryId) { + LibraryType type = LibraryType.fromPatchId(libraryId); + if (type == null) return patch; + + List libraries = new ArrayList<>(); + List rawLibraries = patch.getLibraries(); + for (Library library : rawLibraries) { + if (type.matchLibrary(library, rawLibraries)) { + // skip + } else { + libraries.add(library); + } + } + return patch.withLibraries(libraries); } /** @@ -126,20 +142,24 @@ private Version removingMatchedLibrary(Version version, String libraryId) { */ public LibraryAnalyzer removeLibrary(String libraryId) { if (!has(libraryId)) return this; - version = removingMatchedLibrary(version, libraryId) - .setPatches(version.getPatches().stream() - .filter(patch -> !libraryId.equals(patch.getId())) - .map(patch -> removingMatchedLibrary(patch, libraryId)) - .collect(Collectors.toList())); + GameInstanceManifest manifest = removingMatchedLibrary(this.manifest, libraryId); + this.manifest = manifest.withPatches(this.manifest.getPatches().stream() + .filter(patch -> !libraryId.equals(patch.id())) + .map(patch -> removingMatchedLibrary(patch, libraryId)) + .collect(Collectors.toList())); return this; } - public Version build() { - return version; + public GameInstanceManifest build() { + return manifest; + } + + public static LibraryAnalyzer analyze(GameInstanceManifest.Resolved resolved, String gameVersion) { + return analyze(resolved.standaloneManifest(), gameVersion); } - public static LibraryAnalyzer analyze(Version version, String gameVersion) { - if (version.getInheritsFrom() != null) + public static LibraryAnalyzer analyze(GameInstanceManifest manifest, String gameVersion) { + if (manifest.inheritsFrom() != null) throw new IllegalArgumentException("LibraryAnalyzer can only analyze independent game version"); Map> libraries = new HashMap<>(); @@ -148,27 +168,26 @@ public static LibraryAnalyzer analyze(Version version, String gameVersion) { libraries.put(LibraryType.MINECRAFT.getPatchId(), pair(null, gameVersion)); } - List rawLibraries = version.resolve(null).getLibraries(); + List rawLibraries = manifest.getLibraries(); for (Library library : rawLibraries) { for (LibraryType type : LibraryType.values()) { if (type.matchLibrary(library, rawLibraries)) { - libraries.put(type.getPatchId(), pair(library, type.patchVersion(version, library.getVersion()))); + libraries.put(type.getPatchId(), pair(library, type.patchVersion(manifest, library.getVersion()))); break; } } } - for (Version patch : version.getPatches()) { + for (GameInstancePatch patch : manifest.getPatches()) { if (patch.isHidden()) continue; - libraries.put(patch.getId(), pair(null, patch.getVersion())); + libraries.put(patch.id(), pair(null, patch.version())); } - return new LibraryAnalyzer(version, libraries); + return new LibraryAnalyzer(manifest, libraries); } - public static boolean isModded(VersionProvider provider, Version version) { - Version resolvedVersion = version.resolve(provider); - String mainClass = resolvedVersion.getMainClass(); + public static boolean isModded(GameInstanceManifest.Resolved resolved) { + String mainClass = resolved.launchManifest().mainClass(); return mainClass != null && (LAUNCH_WRAPPER_MAIN.equals(mainClass) || mainClass.startsWith("net.minecraftforge") || mainClass.startsWith("net.neoforged") @@ -223,7 +242,7 @@ protected boolean matchLibrary(Library library, List libraries) { private final Pattern FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?[0-9.]+)(-([0-9.]+))?$"); @Override - protected String patchVersion(Version gameVersion, String libraryVersion) { + protected String patchVersion(GameInstanceManifest gameVersion, String libraryVersion) { Matcher matcher = FORGE_VERSION_MATCHER.matcher(libraryVersion); if (matcher.find()) { return matcher.group("forge"); @@ -246,14 +265,14 @@ protected boolean matchLibrary(Library library, List libraries) { private final Pattern NEO_FORGE_VERSION_MATCHER = Pattern.compile("^([0-9.]+)-(?[0-9.]+)(-([0-9.]+))?$"); @Override - protected String patchVersion(Version gameVersion, String libraryVersion) { + protected String patchVersion(GameInstanceManifest gameVersion, String libraryVersion) { String res = scanVersion(gameVersion); if (res != null) { return res; } - for (Version patch : gameVersion.getPatches()) { - res = scanVersion(patch); + for (GameInstancePatch patch : gameVersion.getPatches()) { + res = scanPatch(patch); if (res != null) { return res; } @@ -267,12 +286,11 @@ protected String patchVersion(Version gameVersion, String libraryVersion) { return super.patchVersion(gameVersion, libraryVersion); } - private String scanVersion(Version version) { - Optional optArgument = version.getArguments(); - if (!optArgument.isPresent()) { + private String scanVersion(GameInstanceManifest manifest) { + if (manifest.arguments() == null) { return null; } - List gameArguments = optArgument.get().game(); + List gameArguments = manifest.arguments().game(); if (gameArguments == null) { return null; } @@ -293,6 +311,32 @@ private String scanVersion(Version version) { return null; } + private String scanPatch(GameInstancePatch patch) { + Arguments optArgument = patch.arguments(); + if (optArgument == null) { + return null; + } + List gameArguments = optArgument.game(); + if (gameArguments == null) { + return null; + } + + for (int i = 0; i < gameArguments.size() - 1; i++) { + Argument argument = gameArguments.get(i); + if (argument instanceof StringArgument) { + String argumentValue = ((StringArgument) argument).argument(); + if ("--fml.neoForgeVersion".equals(argumentValue) || "--fml.forgeVersion".equals(argumentValue)) { + Argument next = gameArguments.get(i + 1); + if (next instanceof StringArgument) { + return ((StringArgument) next).argument(); + } + return null; + } + } + } + return null; + } + }, LITELOADER(true, "liteloader", "com\\.mumfrey", "liteloader", ModLoaderType.LITE_LOADER), OPTIFINE(false, "optifine", "(net\\.)?optifine", "^(?!.*launchwrapper).*$", null), @@ -341,7 +385,7 @@ protected boolean matchLibrary(Library library, List libraries) { return group.matcher(library.getGroupId()).matches() && artifact.matcher(library.getArtifactId()).matches(); } - protected String patchVersion(Version gameVersion, String libraryVersion) { + protected String patchVersion(GameInstanceManifest gameVersion, String libraryVersion) { return libraryVersion; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java index dc57fdf3400..f9d2ec69515 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/MaintainTask.java @@ -38,44 +38,44 @@ import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.*; import static org.jackhuang.hmcl.util.logging.Logger.LOG; -public class MaintainTask extends Task { +public class MaintainTask extends Task { private final GameRepository repository; - private final Version version; + private final GameInstanceManifest manifest; - public MaintainTask(GameRepository repository, Version version) { + public MaintainTask(GameRepository repository, GameInstanceManifest manifest) { this.repository = repository; - this.version = version; + this.manifest = manifest; - if (version.getInheritsFrom() != null) + if (manifest.inheritsFrom() != null) throw new IllegalArgumentException("MaintainTask requires independent game version"); } @Override public void execute() { - setResult(maintain(repository, version)); + setResult(maintain(repository, manifest)); } - public static Version maintain(GameRepository repository, Version version) { - if (version.getInheritsFrom() != null) + public static GameInstanceManifest maintain(GameRepository repository, GameInstanceManifest manifest) { + if (manifest.inheritsFrom() != null) throw new IllegalArgumentException("MaintainTask requires independent game version"); - String mainClass = version.resolve(null).getMainClass(); + String mainClass = manifest.resolve(repository).mainClass(); if (mainClass != null && mainClass.equals(LibraryAnalyzer.LAUNCH_WRAPPER_MAIN)) { - version = maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(repository, unique(version), true), false); + manifest = maintainOptiFineLibrary(repository, maintainGameWithLaunchWrapper(repository, unique(manifest), true), false); } else if (mainClass != null && mainClass.equals(LibraryAnalyzer.MOD_LAUNCHER_MAIN)) { // Forge 1.13 and OptiFine - version = maintainOptiFineLibrary(repository, maintainGameWithCpwModLauncher(repository, unique(version)), true); + manifest = maintainOptiFineLibrary(repository, maintainGameWithCpwModLauncher(repository, unique(manifest)), true); } else if (mainClass != null && mainClass.equals(LibraryAnalyzer.BOOTSTRAP_LAUNCHER_MAIN)) { // Forge 1.17 - version = maintainGameWithCpwBoostrapLauncher(repository, unique(version)); + manifest = maintainGameWithCpwBoostrapLauncher(repository, unique(manifest)); } else { // Vanilla Minecraft does not need maintain // Fabric does not need maintain, nothing compatible with fabric now. - version = maintainOptiFineLibrary(repository, unique(version), false); + manifest = maintainOptiFineLibrary(repository, unique(manifest), false); } - List libraries = version.getLibraries(); + List libraries = manifest.getLibraries(); if (!libraries.isEmpty()) { // HMCL once use log4j-patch to prevent virus. But now, we only modify log4j2.xml. // Therefore, we remove this library. @@ -84,23 +84,23 @@ public static Version maintain(GameRepository repository, Version version) { && ("log4j-patch".equals(library.getArtifactId()) || "log4j-patch-beta9".equals(library.getArtifactId())) && "1.0".equals(library.getVersion()) && library.getDownload() == null) { - version = version.setLibraries(libraries.subList(1, libraries.size())); + manifest = manifest.withLibraries(libraries.subList(1, libraries.size())); } } - return version; + return manifest; } - public static Version maintainPreservingPatches(GameRepository repository, Version version) { - if (!version.isResolvedPreservingPatches()) + public static GameInstanceManifest maintainPreservingPatches(GameRepository repository, GameInstanceManifest manifest) { + if (!manifest.isResolvedPreservingPatches()) throw new IllegalArgumentException("MaintainTask requires independent game version"); - Version newVersion = maintain(repository, version.resolve(repository)); - return newVersion.setPatches(version.getPatches()).markAsUnresolved(); + GameInstanceManifest newVersion = maintain(repository, manifest.resolve(repository)); + return newVersion.withPatches(manifest.getPatches()); } - private static Version maintainGameWithLaunchWrapper(GameRepository repository, Version version, boolean reorderTweakClass) { - LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null); - VersionLibraryBuilder builder = new VersionLibraryBuilder(version); + private static GameInstanceManifest maintainGameWithLaunchWrapper(GameRepository repository, GameInstanceManifest manifest, boolean reorderTweakClass) { + LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(manifest, null); + GameInstanceLibraryBuilder builder = new GameInstanceLibraryBuilder(manifest); String mainClass = null; // Installing Forge will override the Minecraft arguments in json, so LiteLoader and OptiFine Tweaker are being re-added. @@ -143,24 +143,24 @@ private static Version maintainGameWithLaunchWrapper(GameRepository repository, } } - Version ret = builder.build(); - return mainClass == null ? ret : ret.setMainClass(mainClass); + GameInstanceManifest ret = builder.build(); + return mainClass == null ? ret : ret.withMainClass(mainClass); } - private static Version maintainGameWithCpwModLauncher(GameRepository repository, Version version) { - LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null); - VersionLibraryBuilder builder = new VersionLibraryBuilder(version); + private static GameInstanceManifest maintainGameWithCpwModLauncher(GameRepository repository, GameInstanceManifest manifest) { + LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(manifest, null); + GameInstanceLibraryBuilder builder = new GameInstanceLibraryBuilder(manifest); - if (!libraryAnalyzer.has(FORGE)) return version; + if (!libraryAnalyzer.has(FORGE)) return manifest; if (libraryAnalyzer.has(OPTIFINE)) { Library hmclTransformerDiscoveryService = new Library(new Artifact("org.jackhuang.hmcl", "transformer-discovery-service", "1.0")); - Optional optiFine = version.getLibraries().stream().filter(library -> library.is("optifine", "OptiFine")).findAny(); - boolean libraryExisting = version.getLibraries().stream().anyMatch(library -> library.is("org.jackhuang.hmcl", "transformer-discovery-service")); + Optional optiFine = manifest.getLibraries().stream().filter(library -> library.is("optifine", "OptiFine")).findAny(); + boolean libraryExisting = manifest.getLibraries().stream().anyMatch(library -> library.is("org.jackhuang.hmcl", "transformer-discovery-service")); optiFine.ifPresent(library -> { builder.addJvmArgument("-Dhmcl.transformer.candidates=${library_directory}/" + library.getPath()); if (!libraryExisting) builder.addLibrary(hmclTransformerDiscoveryService); - Path libraryPath = repository.getLibraryFile(version, hmclTransformerDiscoveryService); + Path libraryPath = repository.getLibraryFile(manifest, hmclTransformerDiscoveryService); try (InputStream input = MaintainTask.class.getResourceAsStream("/assets/game/HMCLTransformerDiscoveryService-1.0.jar")) { Files.createDirectories(libraryPath.getParent()); Files.copy(Objects.requireNonNull(input, "Bundled HMCLTransformerDiscoveryService is missing."), libraryPath, StandardCopyOption.REPLACE_EXISTING); @@ -173,7 +173,7 @@ private static Version maintainGameWithCpwModLauncher(GameRepository repository, return builder.build(); } - private static String updateIgnoreList(GameRepository repository, Version version, String ignoreList) { + private static String updateIgnoreList(GameRepository repository, GameInstanceManifest manifest, String ignoreList) { String[] ignores = ignoreList.split(","); List newIgnoreList = new ArrayList<>(); @@ -181,12 +181,12 @@ private static String updateIgnoreList(GameRepository repository, Version versio // we need to manually ignore ${primary_jar}. newIgnoreList.add("${primary_jar}"); - Path libraryDirectory = repository.getLibrariesDirectory(version).toAbsolutePath().normalize(); + Path libraryDirectory = repository.getLibrariesDirectory(manifest).toAbsolutePath().normalize(); // The default ignoreList is too loose and may cause some problems, we replace them with the absolute version. // For example, if "client-extra" is in ignoreList, and game directory contains "client-extra" component, all // libraries will be ignored, which is not expected. - for (String classpathName : repository.getClasspath(version)) { + for (String classpathName : repository.getClasspath(manifest)) { Path classpathFile = Paths.get(classpathName).toAbsolutePath(); String fileName = classpathFile.getFileName().toString(); if (Stream.of(ignores).anyMatch(fileName::contains)) { @@ -205,11 +205,11 @@ private static String updateIgnoreList(GameRepository repository, Version versio } // Fix wrong configurations when launching 1.17+ with Forge. - private static Version maintainGameWithCpwBoostrapLauncher(GameRepository repository, Version version) { - LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null); - VersionLibraryBuilder builder = new VersionLibraryBuilder(version); + private static GameInstanceManifest maintainGameWithCpwBoostrapLauncher(GameRepository repository, GameInstanceManifest manifest) { + LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(manifest, null); + GameInstanceLibraryBuilder builder = new GameInstanceLibraryBuilder(manifest); - if (!libraryAnalyzer.has(FORGE) && !libraryAnalyzer.has(NEO_FORGE)) return version; + if (!libraryAnalyzer.has(FORGE) && !libraryAnalyzer.has(NEO_FORGE)) return manifest; Optional bslVersion = libraryAnalyzer.getVersion(BOOTSTRAP_LAUNCHER); @@ -224,7 +224,7 @@ private static Version maintainGameWithCpwBoostrapLauncher(GameRepository reposi if (jvmArg instanceof StringArgument) { String jvmArgStr = jvmArg.toString(); if (jvmArgStr.startsWith("-DignoreList=")) { - jvm.set(i, new StringArgument("-DignoreList=" + updateIgnoreList(repository, version, jvmArgStr.substring("-DignoreList=".length())))); + jvm.set(i, new StringArgument("-DignoreList=" + updateIgnoreList(repository, manifest, jvmArgStr.substring("-DignoreList=".length())))); } } } @@ -247,20 +247,20 @@ private static Version maintainGameWithCpwBoostrapLauncher(GameRepository reposi return builder.build(); } - private static Version maintainOptiFineLibrary(GameRepository repository, Version version, boolean remove) { - LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(version, null); - List libraries = new ArrayList<>(version.getLibraries()); + private static GameInstanceManifest maintainOptiFineLibrary(GameRepository repository, GameInstanceManifest manifest, boolean remove) { + LibraryAnalyzer libraryAnalyzer = LibraryAnalyzer.analyze(manifest, null); + List libraries = new ArrayList<>(manifest.getLibraries()); if (libraryAnalyzer.has(OPTIFINE)) { if (libraryAnalyzer.has(LITELOADER) || libraryAnalyzer.has(FORGE)) { // If forge or LiteLoader installed, OptiFine Forge Tweaker is needed. // And we should load the installer jar instead of patch jar. if (repository != null) { - for (int i = 0; i < version.getLibraries().size(); ++i) { + for (int i = 0; i < manifest.getLibraries().size(); ++i) { Library library = libraries.get(i); if (library.is("optifine", "OptiFine")) { Library newLibrary = new Library(new Artifact("optifine", "OptiFine", library.getVersion(), "installer")); - if (Files.exists(repository.getLibraryFile(version, newLibrary))) { + if (Files.exists(repository.getLibraryFile(manifest, newLibrary))) { libraries.set(i, null); // OptiFine should be loaded after Forge in classpath. // Although we have altered priority of OptiFine higher than Forge, @@ -281,15 +281,15 @@ private static Version maintainOptiFineLibrary(GameRepository repository, Versio } } - return version.setLibraries(libraries.stream().filter(Objects::nonNull).collect(Collectors.toList())); + return manifest.withLibraries(libraries.stream().filter(Objects::nonNull).collect(Collectors.toList())); } - public static Version unique(Version version) { + public static GameInstanceManifest unique(GameInstanceManifest manifest) { List libraries = new ArrayList<>(); SimpleMultimap> multimap = new SimpleMultimap<>(HashMap::new, ArrayList::new); - for (Library library : version.getLibraries()) { + for (Library library : manifest.getLibraries()) { String id = library.getGroupId() + ":" + library.getArtifactId(); VersionNumber number = VersionNumber.asVersion(library.getVersion()); String serialized = JsonUtils.GSON.toJson(library); @@ -336,6 +336,6 @@ public static Version unique(Version version) { } } - return version.setLibraries(libraries); + return manifest.withLibraries(libraries); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java index 99e39a2184c..c3bf1da8e08 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/RemoteVersion.java @@ -17,7 +17,8 @@ */ package org.jackhuang.hmcl.download; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.versioning.VersionNumber; @@ -95,7 +96,7 @@ public Type getVersionType() { return type; } - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { throw new UnsupportedOperationException(this + " cannot be installed yet"); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java index 15bd9e80163..a7d7ff82733 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomInstallTask.java @@ -23,7 +23,8 @@ import org.jackhuang.hmcl.download.VersionMismatchException; import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile; import org.jackhuang.hmcl.download.forge.ForgeNewInstallTask; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -38,27 +39,27 @@ import java.util.Map; import java.util.Optional; -public final class CleanroomInstallTask extends Task { +public final class CleanroomInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final CleanroomRemoteVersion remote; private Path installer; private FileDownloadTask dependent; - private Task task; + private Task task; private String selfVersion; - public CleanroomInstallTask(DefaultDependencyManager dependencyManager, Version version, CleanroomRemoteVersion remoteVersion) { + public CleanroomInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, CleanroomRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; setSignificance(TaskSignificance.MODERATE); } - public CleanroomInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) { + public CleanroomInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, String selfVersion, Path installer) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.selfVersion = selfVersion; this.remote = null; this.installer = installer; @@ -112,14 +113,14 @@ public Collection> getDependencies() { @Override public void execute() throws IOException, VersionMismatchException, UnsupportedInstallationException { if (selfVersion == null) { - task = new ForgeNewInstallTask(dependencyManager, version, remote.getSelfVersion(), installer).thenApplyAsync((version) -> version.setId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId())); + task = new ForgeNewInstallTask(dependencyManager, manifest, remote.getSelfVersion(), installer).thenApplyAsync((version) -> version.withId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId())); } else { - task = new ForgeNewInstallTask(dependencyManager, version, selfVersion, installer).thenApplyAsync((version) -> version.setId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId())); + task = new ForgeNewInstallTask(dependencyManager, manifest, selfVersion, installer).thenApplyAsync((version) -> version.withId(LibraryAnalyzer.LibraryType.CLEANROOM.getPatchId())); } } - public static Task install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException { - Optional gameVersion = dependencyManager.getGameRepository().getGameVersion(version); + public static Task install(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, Path installer) throws IOException, VersionMismatchException { + Optional gameVersion = dependencyManager.getGameRepository().getGameVersion(manifest); if (gameVersion.isEmpty()) throw new IOException(); try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { String installProfileText = Files.readString(fs.getPath("install_profile.json")); @@ -128,7 +129,7 @@ public static Task install(DefaultDependencyManager dependencyManager, ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); if (!gameVersion.get().equals(profile.getMinecraft())) throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get()); - return new CleanroomInstallTask(dependencyManager, version, modifyVersion(profile.getVersion()), installer); + return new CleanroomInstallTask(dependencyManager, manifest, modifyVersion(profile.getVersion()), installer); } else { throw new IOException(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java index 6d6bfddbd4e..3f88ef351cd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/cleanroom/CleanroomRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; import java.time.Instant; @@ -32,7 +33,7 @@ public CleanroomRemoteVersion(String gameVersion, String selfVersion, Instant re } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new CleanroomInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIInstallTask.java index d8e09955711..6e3c67e8521 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIInstallTask.java @@ -18,7 +18,8 @@ package org.jackhuang.hmcl.download.fabric; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; @@ -32,16 +33,16 @@ * * @author huangyuhui */ -public final class FabricAPIInstallTask extends Task { +public final class FabricAPIInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final FabricAPIRemoteVersion remote; private final List> dependencies = new ArrayList<>(1); - public FabricAPIInstallTask(DefaultDependencyManager dependencyManager, Version version, FabricAPIRemoteVersion remoteVersion) { + public FabricAPIInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, FabricAPIRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; } @@ -59,7 +60,7 @@ public boolean isRelyingOnDependencies() { public void execute() throws IOException { dependencies.add(new FileDownloadTask( remote.getVersion().file().url(), - dependencyManager.getGameRepository().getModsDirectory(version.getId()).resolve("fabric-api-" + remote.getVersion().version() + ".jar"), + dependencyManager.getGameRepository().getModsDirectory(manifest.id()).resolve("fabric-api-" + remote.getVersion().version() + ".jar"), remote.getVersion().file().getIntegrityCheck()) ); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java index 3cbe9569053..db2e955ccc3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricAPIRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.task.Task; @@ -55,7 +56,7 @@ public RemoteAddon.Version getVersion() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new FabricAPIInstallTask(dependencyManager, baseVersion, this); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricInstallTask.java index b21586201ac..72c8764cee6 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricInstallTask.java @@ -22,10 +22,12 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.UnsupportedInstallationException; +import org.jackhuang.hmcl.download.game.GameLibrariesTask; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.Artifact; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonSerializable; @@ -41,17 +43,17 @@ * * @author huangyuhui */ -public final class FabricInstallTask extends Task { +public final class FabricInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final FabricRemoteVersion remote; private final GetTask launchMetaTask; private final List> dependencies = new ArrayList<>(1); - public FabricInstallTask(DefaultDependencyManager dependencyManager, Version version, FabricRemoteVersion remoteVersion) { + public FabricInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, FabricRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; launchMetaTask = new GetTask(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls())); @@ -65,7 +67,7 @@ public boolean doPreExecute() { @Override public void preExecute() throws Exception { - if (!Objects.equals("net.minecraft.client.main.Main", version.resolve(dependencyManager.getGameRepository()).getMainClass())) + if (!Objects.equals("net.minecraft.client.main.Main", manifest.resolve(dependencyManager.getGameRepository()).mainClass())) throw new UnsupportedInstallationException(FABRIC_NOT_COMPATIBLE_WITH_FORGE); } @@ -92,10 +94,10 @@ public void execute() throws IOException { setResult(getPatch(fabricInfo, remote.getGameVersion(), remote.getSelfVersion())); - dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true)); + dependencies.add(new GameLibrariesTask(dependencyManager, manifest, true, getResult().getLibraries())); } - private Version getPatch(FabricInfo fabricInfo, String gameVersion, String loaderVersion) { + private GameInstancePatch getPatch(FabricInfo fabricInfo, String gameVersion, String loaderVersion) { JsonObject launcherMeta = fabricInfo.launcherMeta; Arguments arguments = new Arguments(); @@ -125,7 +127,7 @@ private Version getPatch(FabricInfo fabricInfo, String gameVersion, String loade libraries.add(new Library(Artifact.fromDescriptor(fabricInfo.intermediary.maven), "https://maven.fabricmc.net/", null)); libraries.add(new Library(Artifact.fromDescriptor(fabricInfo.loader.maven), "https://maven.fabricmc.net/", null)); - return new Version(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), loaderVersion, Version.PRIORITY_LOADER, arguments, mainClass, libraries); + return new GameInstancePatch(LibraryAnalyzer.LibraryType.FABRIC.getPatchId(), loaderVersion, GameInstancePatch.PRIORITY_LOADER, arguments, mainClass, libraries); } @JsonSerializable diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java index 015edd80142..857c593eeaf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/fabric/FabricRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; import java.util.List; @@ -38,7 +39,7 @@ public class FabricRemoteVersion extends RemoteVersion { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new FabricInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java index 2053837ddc5..6757655f796 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallProfile.java @@ -19,7 +19,7 @@ import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.gson.Validation; @@ -34,9 +34,9 @@ public final class ForgeInstallProfile implements Validation { private final ForgeInstall install; @SerializedName("versionInfo") - private final Version versionInfo; + private final GameInstanceManifest versionInfo; - public ForgeInstallProfile(ForgeInstall install, Version versionInfo) { + public ForgeInstallProfile(ForgeInstall install, GameInstanceManifest versionInfo) { this.install = install; this.versionInfo = versionInfo; } @@ -45,7 +45,7 @@ public ForgeInstall getInstall() { return install; } - public Version getVersionInfo() { + public GameInstanceManifest getVersionInfo() { return versionInfo; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java index 922fb75f2aa..bff618bfb83 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeInstallTask.java @@ -18,7 +18,8 @@ package org.jackhuang.hmcl.download.forge; import org.jackhuang.hmcl.download.*; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -42,18 +43,18 @@ * * @author huangyuhui */ -public final class ForgeInstallTask extends Task { +public final class ForgeInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private Path installer; private final ForgeRemoteVersion remote; private FileDownloadTask dependent; - private Task dependency; + private Task dependency; - public ForgeInstallTask(DefaultDependencyManager dependencyManager, Version version, ForgeRemoteVersion remoteVersion) { + public ForgeInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, ForgeRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; setSignificance(TaskSignificance.MODERATE); } @@ -98,31 +99,31 @@ public Collection> getDependencies() { @Override public void execute() throws IOException, VersionMismatchException, UnsupportedInstallationException { - String originalMainClass = version.resolve(dependencyManager.getGameRepository()).getMainClass(); + String originalMainClass = manifest.resolve(dependencyManager.getGameRepository()).mainClass(); if (GameVersionNumber.compare("1.13", remote.getGameVersion()) <= 0) { // Forge 1.13 is not compatible with fabric. if (!LibraryAnalyzer.FORGE_OPTIFINE_MAIN.contains(originalMainClass)) throw new UnsupportedInstallationException(UNSUPPORTED_LAUNCH_WRAPPER); } - if (detectForgeInstallerType(dependencyManager, version, installer)) - dependency = new ForgeNewInstallTask(dependencyManager, version, remote.getSelfVersion(), installer); + if (detectForgeInstallerType(dependencyManager, manifest, installer)) + dependency = new ForgeNewInstallTask(dependencyManager, manifest, remote.getSelfVersion(), installer); else - dependency = new ForgeOldInstallTask(dependencyManager, version, remote.getSelfVersion(), installer); + dependency = new ForgeOldInstallTask(dependencyManager, manifest, remote.getSelfVersion(), installer); } /** * Detect Forge installer type. * * @param dependencyManager game repository - * @param version version.json + * @param manifest instance manifest * @param installer the Forge installer, either the new or old one. * @return true for new, false for old * @throws IOException if unable to read compressed content of installer file, or installer file is corrupted, or the installer is not the one we want. * @throws VersionMismatchException if required game version of installer does not match the actual one. */ - public static boolean detectForgeInstallerType(DependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException { - Optional gameVersion = dependencyManager.getGameRepository().getGameVersion(version); + public static boolean detectForgeInstallerType(DependencyManager dependencyManager, GameInstanceManifest manifest, Path installer) throws IOException, VersionMismatchException { + Optional gameVersion = dependencyManager.getGameRepository().getGameVersion(manifest); if (!gameVersion.isPresent()) throw new IOException(); try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { String installProfileText = Files.readString(fs.getPath("install_profile.json")); @@ -148,14 +149,14 @@ public static boolean detectForgeInstallerType(DependencyManager dependencyManag * This method will try to identify this installer whether it is in old or new format. * * @param dependencyManager game repository - * @param version version.json + * @param manifest instance manifest * @param installer the Forge installer, either the new or old one. * @return the task to install library * @throws IOException if unable to read compressed content of installer file, or installer file is corrupted, or the installer is not the one we want. * @throws VersionMismatchException if required game version of installer does not match the actual one. */ - public static Task install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException { - Optional gameVersion = dependencyManager.getGameRepository().getGameVersion(version); + public static Task install(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, Path installer) throws IOException, VersionMismatchException { + Optional gameVersion = dependencyManager.getGameRepository().getGameVersion(manifest); if (!gameVersion.isPresent()) throw new IOException(); try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { String installProfileText = Files.readString(fs.getPath("install_profile.json")); @@ -164,12 +165,12 @@ public static Task install(DefaultDependencyManager dependencyManager, ForgeNewInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeNewInstallProfile.class); if (!gameVersion.get().equals(profile.getMinecraft())) throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get()); - return new ForgeNewInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getVersion()), installer); + return new ForgeNewInstallTask(dependencyManager, manifest, modifyVersion(gameVersion.get(), profile.getVersion()), installer); } else if (installProfile.containsKey("install") && installProfile.containsKey("versionInfo")) { ForgeInstallProfile profile = JsonUtils.fromNonNullJson(installProfileText, ForgeInstallProfile.class); if (!gameVersion.get().equals(profile.getInstall().getMinecraft())) throw new VersionMismatchException(profile.getInstall().getMinecraft(), gameVersion.get()); - return new ForgeOldInstallTask(dependencyManager, version, modifyVersion(gameVersion.get(), profile.getInstall().getPath().getVersion().replaceAll("(?i)forge", "")), installer); + return new ForgeOldInstallTask(dependencyManager, manifest, modifyVersion(gameVersion.get(), profile.getInstall().getPath().getVersion().replaceAll("(?i)forge", "")), installer); } else { throw new IOException(); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java index ccea924d195..50b18ac8a33 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeNewInstallTask.java @@ -22,13 +22,14 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile.Processor; import org.jackhuang.hmcl.download.game.GameLibrariesTask; -import org.jackhuang.hmcl.download.game.VersionJsonDownloadTask; +import org.jackhuang.hmcl.download.game.GameInstanceJsonDownloadTask; import org.jackhuang.hmcl.game.Artifact; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.game.DownloadInfo; import org.jackhuang.hmcl.game.DownloadType; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; @@ -62,7 +63,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.gson.JsonUtils.fromNonNullJson; -public class ForgeNewInstallTask extends Task { +public class ForgeNewInstallTask extends Task { private class ProcessorTask extends Task { @@ -115,7 +116,7 @@ public void execute() throws Exception { return; } - Path jar = gameRepository.getArtifactFile(version, processor.getJar()); + Path jar = gameRepository.getArtifactFile(manifest, processor.getJar()); if (!Files.isRegularFile(jar)) throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess"); @@ -133,7 +134,7 @@ public void execute() throws Exception { List classpath = new ArrayList<>(processor.getClasspath().size() + 1); for (Artifact artifact : processor.getClasspath()) { - Path file = gameRepository.getArtifactFile(version, artifact); + Path file = gameRepository.getArtifactFile(manifest, artifact); if (!Files.isRegularFile(file)) throw new Exception("Game processor dependency missing"); classpath.add(file.toString()); @@ -194,23 +195,23 @@ public void execute() throws Exception { private final DefaultDependencyManager dependencyManager; private final DefaultGameRepository gameRepository; - private final Version version; + private final GameInstanceManifest manifest; private final Path installer; private final List> dependents = new ArrayList<>(1); private final List> dependencies = new ArrayList<>(1); private ForgeNewInstallProfile profile; private List processors; - private Version forgeVersion; + private GameInstanceManifest forgeVersion; private final String selfVersion; private Path tempDir; private AtomicInteger processorDoneCount = new AtomicInteger(0); - public ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) { + public ForgeNewInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, String selfVersion, Path installer) { this.dependencyManager = dependencyManager; this.gameRepository = dependencyManager.getGameRepository(); - this.version = version; + this.manifest = manifest; this.installer = installer; this.selfVersion = selfVersion; @@ -267,7 +268,7 @@ private String parseLiteral(String literal, Map parseOptions(List args, Map vars) { @@ -354,9 +355,9 @@ private Task patchDownloadMojangMappingsTask(Processor processor, Map { - DownloadInfo mappings = fromNonNullJson(json, Version.class) + DownloadInfo mappings = fromNonNullJson(json, GameInstanceManifest.class) .getDownloads().get(DownloadType.CLIENT_MAPPINGS); if (mappings == null) { throw new Exception("client_mappings download info not found"); @@ -408,11 +409,11 @@ public void execute() throws Exception { } vars.put("SIDE", "client"); - vars.put("MINECRAFT_JAR", FileUtils.getAbsolutePath(gameRepository.getVersionJar(version))); - vars.put("MINECRAFT_VERSION", FileUtils.getAbsolutePath(gameRepository.getVersionJar(version))); + vars.put("MINECRAFT_JAR", FileUtils.getAbsolutePath(gameRepository.getInstanceJar(manifest))); + vars.put("MINECRAFT_VERSION", FileUtils.getAbsolutePath(gameRepository.getInstanceJar(manifest))); vars.put("ROOT", FileUtils.getAbsolutePath(gameRepository.getBaseDirectory())); vars.put("INSTALLER", installer.toAbsolutePath().toString()); - vars.put("LIBRARY_DIR", FileUtils.getAbsolutePath(gameRepository.getLibrariesDirectory(version))); + vars.put("LIBRARY_DIR", FileUtils.getAbsolutePath(gameRepository.getLibrariesDirectory(manifest))); updateProgress(0, processors.size()); @@ -425,10 +426,11 @@ public void execute() throws Exception { processorsTask.thenComposeAsync( dependencyManager.checkLibraryCompletionAsync(forgeVersion, true))); - setResult(forgeVersion - .setPriority(Version.PRIORITY_LOADER) - .setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId()) - .setVersion(selfVersion)); + setResult(GameInstancePatch.fromManifest( + forgeVersion, + LibraryAnalyzer.LibraryType.FORGE.getPatchId(), + selfVersion, + GameInstancePatch.PRIORITY_LOADER)); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java index 61aa3ea3350..1e2430dcc82 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeOldInstallTask.java @@ -20,8 +20,9 @@ import org.jackhuang.hmcl.download.ArtifactMalformedException; import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -35,17 +36,17 @@ import java.util.zip.ZipException; import java.util.zip.ZipFile; -public class ForgeOldInstallTask extends Task { +public class ForgeOldInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final Path installer; private final String selfVersion; private final List> dependencies = new ArrayList<>(1); - ForgeOldInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) { + ForgeOldInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, String selfVersion, Path installer) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.installer = installer; this.selfVersion = selfVersion; @@ -72,7 +73,7 @@ public void execute() throws Exception { // unpack the universal jar in the installer file. Library forgeLibrary = new Library(installProfile.getInstall().getPath()); - Path forgeFile = dependencyManager.getGameRepository().getLibraryFile(version, forgeLibrary); + Path forgeFile = dependencyManager.getGameRepository().getLibraryFile(manifest, forgeLibrary); Files.createDirectories(forgeFile.getParent()); ZipEntry forgeEntry = zipFile.getEntry(installProfile.getInstall().getFilePath()); @@ -81,10 +82,11 @@ public void execute() throws Exception { is.transferTo(os); } - setResult(installProfile.getVersionInfo() - .setPriority(Version.PRIORITY_LOADER) - .setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId()) - .setVersion(selfVersion)); + setResult(GameInstancePatch.fromManifest( + installProfile.getVersionInfo(), + LibraryAnalyzer.LibraryType.FORGE.getPatchId(), + selfVersion, + GameInstancePatch.PRIORITY_LOADER)); dependencies.add(dependencyManager.checkLibraryCompletionAsync(installProfile.getVersionInfo(), true)); } catch (ZipException ex) { throw new ArtifactMalformedException("Malformed forge installer file", ex); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java index 5550506fc6f..34278a822c0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/forge/ForgeRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; import java.time.Instant; @@ -39,7 +40,7 @@ public ForgeRemoteVersion(String gameVersion, String selfVersion, Instant releas } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new ForgeInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java index a366af04a1d..8d87159a23c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetDownloadTask.java @@ -19,10 +19,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.AbstractDependencyManager; -import org.jackhuang.hmcl.game.AssetIndex; -import org.jackhuang.hmcl.game.AssetIndexInfo; -import org.jackhuang.hmcl.game.AssetObject; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.CacheRepository; @@ -45,7 +42,7 @@ public final class GameAssetDownloadTask extends Task { private final AbstractDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final AssetIndexInfo assetIndexInfo; private final Path assetIndexFile; private final boolean integrityCheck; @@ -55,18 +52,18 @@ public final class GameAssetDownloadTask extends Task { /** * Constructor. * - * @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository} - * @param version the game version + * @param dependencyManager the dependency manager that can provides {@link GameRepository} + * @param manifest the game version */ - public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, Version version, boolean forceDownloadingIndex, boolean integrityCheck) { + public GameAssetDownloadTask(AbstractDependencyManager dependencyManager, GameInstanceManifest manifest, boolean forceDownloadingIndex, boolean integrityCheck) { this.dependencyManager = dependencyManager; - this.version = version.resolve(dependencyManager.getGameRepository()); - this.assetIndexInfo = this.version.getAssetIndex(); - this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId()); + this.manifest = manifest.resolve(dependencyManager.getGameRepository()); + this.assetIndexInfo = this.manifest.getAssetIndex(); + this.assetIndexFile = dependencyManager.getGameRepository().getIndexFile(manifest.id(), assetIndexInfo.getId()); this.integrityCheck = integrityCheck; setStage("hmcl.install.assets"); - dependents.add(new GameAssetIndexDownloadTask(dependencyManager, this.version, forceDownloadingIndex)); + dependents.add(new GameAssetIndexDownloadTask(dependencyManager, this.manifest, forceDownloadingIndex)); } @Override @@ -93,7 +90,7 @@ public void execute() throws Exception { if (isCancelled()) throw new InterruptedException(); - Path file = dependencyManager.getGameRepository().getAssetObject(version.getId(), assetIndexInfo.getId(), assetObject); + Path file = dependencyManager.getGameRepository().getAssetObject(manifest.id(), assetIndexInfo.getId(), assetObject); boolean download = !Files.isRegularFile(file); try { if (!download && integrityCheck && !assetObject.validateChecksum(file, true)) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetIndexDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetIndexDownloadTask.java index 88beb8b6916..110e99b72e2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetIndexDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameAssetIndexDownloadTask.java @@ -21,7 +21,8 @@ import org.jackhuang.hmcl.download.AbstractDependencyManager; import org.jackhuang.hmcl.game.AssetIndex; import org.jackhuang.hmcl.game.AssetIndexInfo; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; @@ -44,19 +45,19 @@ public final class GameAssetIndexDownloadTask extends Task { private final AbstractDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final boolean forceDownloading; private final List> dependencies = new ArrayList<>(1); /** * Constructor. * - * @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository} - * @param version the resolved version + * @param dependencyManager the dependency manager that can provides {@link GameRepository} + * @param manifest the resolved version */ - public GameAssetIndexDownloadTask(AbstractDependencyManager dependencyManager, Version version, boolean forceDownloading) { + public GameAssetIndexDownloadTask(AbstractDependencyManager dependencyManager, GameInstanceManifest manifest, boolean forceDownloading) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.forceDownloading = forceDownloading; setSignificance(TaskSignificance.MODERATE); } @@ -68,8 +69,8 @@ public List> getDependencies() { @Override public void execute() { - AssetIndexInfo assetIndexInfo = version.getAssetIndex(); - Path assetIndexFile = dependencyManager.getGameRepository().getIndexFile(version.getId(), assetIndexInfo.getId()); + AssetIndexInfo assetIndexInfo = manifest.getAssetIndex(); + Path assetIndexFile = dependencyManager.getGameRepository().getIndexFile(manifest.id(), assetIndexInfo.getId()); boolean verifyHashCode = StringUtils.isNotBlank(assetIndexInfo.getSha1()) && assetIndexInfo.getUrl().contains(assetIndexInfo.getSha1()); if (Files.exists(assetIndexFile) && !forceDownloading) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java index c50596836e4..5bb59fa6474 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameDownloadTask.java @@ -18,7 +18,7 @@ package org.jackhuang.hmcl.download.game; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.CacheRepository; @@ -35,13 +35,13 @@ public final class GameDownloadTask extends Task { private final DefaultDependencyManager dependencyManager; private final String gameVersion; - private final Version version; + private final GameInstanceManifest manifest; private final List> dependencies = new ArrayList<>(); - public GameDownloadTask(DefaultDependencyManager dependencyManager, String gameVersion, Version version) { + public GameDownloadTask(DefaultDependencyManager dependencyManager, String gameVersion, GameInstanceManifest manifest) { this.dependencyManager = dependencyManager; this.gameVersion = gameVersion; - this.version = version.resolve(dependencyManager.getGameRepository()); + this.manifest = manifest.resolve(dependencyManager.getGameRepository()); setSignificance(TaskSignificance.MODERATE); } @@ -53,12 +53,12 @@ public Collection> getDependencies() { @Override public void execute() { - Path jar = dependencyManager.getGameRepository().getVersionJar(version); + Path jar = dependencyManager.getGameRepository().getInstanceJar(manifest); var task = new FileDownloadTask( - dependencyManager.getDownloadProvider().injectURLWithCandidates(version.getDownloadInfo().getUrl()), + dependencyManager.getDownloadProvider().injectURLWithCandidates(manifest.getDownloadInfo().getUrl()), jar, - FileDownloadTask.IntegrityCheck.of(CacheRepository.SHA1, version.getDownloadInfo().getSha1())); + FileDownloadTask.IntegrityCheck.of(CacheRepository.SHA1, manifest.getDownloadInfo().getSha1())); task.setCaching(true); task.setCacheRepository(dependencyManager.getCacheRepository()); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstallTask.java index aea0e045422..84597510715 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstallTask.java @@ -19,7 +19,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -30,21 +31,21 @@ import static org.jackhuang.hmcl.download.LibraryAnalyzer.LibraryType.MINECRAFT; -public class GameInstallTask extends Task { +public class GameInstallTask extends Task { private final DefaultGameRepository gameRepository; private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final GameRemoteVersion remote; - private final VersionJsonDownloadTask downloadTask; + private final GameInstanceJsonDownloadTask downloadTask; private final List> dependencies = new ArrayList<>(1); - public GameInstallTask(DefaultDependencyManager dependencyManager, Version version, GameRemoteVersion remoteVersion) { + public GameInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, GameRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; this.gameRepository = dependencyManager.getGameRepository(); - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; - this.downloadTask = new VersionJsonDownloadTask(remoteVersion.getGameVersion(), dependencyManager); + this.downloadTask = new GameInstanceJsonDownloadTask(remoteVersion.getGameVersion(), dependencyManager); } @Override @@ -64,11 +65,14 @@ public boolean isRelyingOnDependencies() { @Override public void execute() throws Exception { - Version patch = JsonUtils.fromNonNullJson(downloadTask.getResult(), Version.class) - .setId(MINECRAFT.getPatchId()).setVersion(remote.getGameVersion()).setJar(null).setPriority(Version.PRIORITY_MC); + GameInstancePatch patch = GameInstancePatch.fromManifest( + JsonUtils.fromNonNullJson(downloadTask.getResult(), GameInstanceManifest.class), + MINECRAFT.getPatchId(), + remote.getGameVersion(), + GameInstancePatch.PRIORITY_MC).withJar(null); setResult(patch); - Version version = new Version(this.version.getId()).addPatch(patch); + GameInstanceManifest version = new GameInstanceManifest(this.manifest.id()).addPatch(patch); dependencies.add(Task.allOf( new GameDownloadTask(dependencyManager, remote.getGameVersion(), version), Task.allOf( diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java similarity index 93% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java index 112295a1506..046159feda0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonDownloadTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameInstanceJsonDownloadTask.java @@ -32,14 +32,14 @@ * * @author huangyuhui */ -public final class VersionJsonDownloadTask extends Task { +public final class GameInstanceJsonDownloadTask extends Task { private final String gameVersion; private final DefaultDependencyManager dependencyManager; private final List> dependents = new ArrayList<>(1); private final List> dependencies = new ArrayList<>(1); private final VersionList gameVersionList; - public VersionJsonDownloadTask(String gameVersion, DefaultDependencyManager dependencyManager) { + public GameInstanceJsonDownloadTask(String gameVersion, DefaultDependencyManager dependencyManager) { this.gameVersion = gameVersion; this.dependencyManager = dependencyManager; this.gameVersionList = dependencyManager.getVersionList("game"); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java index d7305f1cc1b..d4c9151bc32 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameLibrariesTask.java @@ -21,9 +21,9 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.MaintainTask; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.DigestUtils; @@ -55,7 +55,7 @@ public final class GameLibrariesTask extends Task { private final AbstractDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final boolean integrityCheck; private final List libraries; private final List> dependencies = new ArrayList<>(); @@ -63,22 +63,22 @@ public final class GameLibrariesTask extends Task { /** * Constructor. * - * @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository} - * @param version the game version + * @param dependencyManager the dependency manager that can provides {@link GameRepository} + * @param manifest the game version */ - public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, boolean integrityCheck) { - this(dependencyManager, version, integrityCheck, version.resolve(dependencyManager.getGameRepository()).getLibraries()); + public GameLibrariesTask(AbstractDependencyManager dependencyManager, GameInstanceManifest manifest, boolean integrityCheck) { + this(dependencyManager, manifest, integrityCheck, manifest.resolve(dependencyManager.getGameRepository()).getLibraries()); } /** * Constructor. * - * @param dependencyManager the dependency manager that can provides {@link org.jackhuang.hmcl.game.GameRepository} - * @param version the game version + * @param dependencyManager the dependency manager that can provides {@link GameRepository} + * @param manifest the game version */ - public GameLibrariesTask(AbstractDependencyManager dependencyManager, Version version, boolean integrityCheck, List libraries) { + public GameLibrariesTask(AbstractDependencyManager dependencyManager, GameInstanceManifest manifest, boolean integrityCheck, List libraries) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.integrityCheck = integrityCheck; this.libraries = libraries; @@ -91,8 +91,8 @@ public List> getDependencies() { return dependencies; } - public static boolean shouldDownloadLibrary(GameRepository gameRepository, Version version, Library library, boolean integrityCheck) { - Path file = gameRepository.getLibraryFile(version, library); + public static boolean shouldDownloadLibrary(GameRepository gameRepository, GameInstanceManifest manifest, Library library, boolean integrityCheck) { + Path file = gameRepository.getLibraryFile(manifest, library); if (!Files.isRegularFile(file)) return true; if (!integrityCheck) { @@ -161,9 +161,9 @@ public void execute() throws IOException { } } - Path file = gameRepository.getLibraryFile(version, library); - if ("optifine".equals(library.getGroupId()) && Files.exists(file) && GameVersionNumber.asGameVersion(gameRepository.getGameVersion(version)).compareTo("1.20.4") == 0) { - String forgeVersion = LibraryAnalyzer.analyze(version, "1.20.4") + Path file = gameRepository.getLibraryFile(manifest, library); + if ("optifine".equals(library.getGroupId()) && Files.exists(file) && GameVersionNumber.asGameVersion(gameRepository.getGameVersion(manifest).orElse(null)).compareTo("1.20.4") == 0) { + String forgeVersion = LibraryAnalyzer.analyze(manifest, "1.20.4") .getVersion(LibraryAnalyzer.LibraryType.FORGE) .orElse(null); if (forgeVersion != null && LibraryAnalyzer.FORGE_OPTIFINE_BROKEN_RANGE.contains(VersionNumber.asVersion(forgeVersion))) { @@ -182,7 +182,7 @@ public void execute() throws IOException { } } - if (shouldDownloadLibrary(gameRepository, version, library, integrityCheck) && (library.hasDownloadURL() || !"optifine".equals(library.getGroupId()))) { + if (shouldDownloadLibrary(gameRepository, manifest, library, integrityCheck) && (library.hasDownloadURL() || !"optifine".equals(library.getGroupId()))) { dependencies.add(new LibraryDownloadTask(dependencyManager, file, library).withCounter("hmcl.install.libraries")); } else { dependencyManager.getCacheRepository().tryCacheLibrary(library, file); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java index 43ef82d58a8..386ac9367ad 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameRemoteVersion.java @@ -20,8 +20,9 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.game.ReleaseType; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -48,7 +49,7 @@ public ReleaseType getType() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new GameInstallTask(dependencyManager, baseVersion, this); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVerificationFixTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVerificationFixTask.java index 33407066e58..32d40f74181 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVerificationFixTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/GameVerificationFixTask.java @@ -19,7 +19,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.CompressingUtils; import org.jackhuang.hmcl.util.versioning.GameVersionNumber; @@ -40,17 +40,13 @@ public final class GameVerificationFixTask extends Task { private final DefaultDependencyManager dependencyManager; private final String gameVersion; - private final Version version; + private final GameInstanceManifest manifest; private final List> dependencies = new ArrayList<>(); - public GameVerificationFixTask(DefaultDependencyManager dependencyManager, String gameVersion, Version version) { + public GameVerificationFixTask(DefaultDependencyManager dependencyManager, String gameVersion, GameInstanceManifest manifest) { this.dependencyManager = dependencyManager; this.gameVersion = gameVersion; - this.version = version; - - if (!version.isResolved()) { - throw new IllegalArgumentException("GameVerificationFixTask requires a resolved game version"); - } + this.manifest = manifest; setSignificance(TaskSignificance.MODERATE); } @@ -62,8 +58,8 @@ public Collection> getDependencies() { @Override public void execute() throws IOException { - Path jar = dependencyManager.getGameRepository().getVersionJar(version); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(version, gameVersion); + Path jar = dependencyManager.getGameRepository().getInstanceJar(manifest); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(manifest, gameVersion); if (Files.exists(jar) && GameVersionNumber.compare(gameVersion, "1.6") < 0 && analyzer.has(LibraryAnalyzer.LibraryType.FORGE)) { try (FileSystem fs = CompressingUtils.createWritableZipFileSystem(jar, StandardCharsets.UTF_8)) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonSaveTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonSaveTask.java deleted file mode 100644 index c60265947be..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/game/VersionJsonSaveTask.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui 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 . - */ -package org.jackhuang.hmcl.download.game; - -import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.game.Version; -import org.jackhuang.hmcl.task.Task; -import org.jackhuang.hmcl.util.gson.JsonUtils; - -import java.nio.file.Files; -import java.nio.file.Path; - -/** - * This task is to save the version json. - * - * @author huangyuhui - */ -public final class VersionJsonSaveTask extends Task { - - private final DefaultGameRepository repository; - private final Version version; - - /** - * Constructor. - * - * @param repository the game repository - * @param version the game version - */ - public VersionJsonSaveTask(DefaultGameRepository repository, Version version) { - this.repository = repository; - this.version = version; - - setSignificance(TaskSignificance.MODERATE); - setResult(version); - } - - @Override - public void execute() throws Exception { - Path json = repository.getVersionJson(version.getId()).toAbsolutePath(); - Files.createDirectories(json.getParent()); - JsonUtils.writeToJsonFile(json, version); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIInstallTask.java index 1a19df5953f..d11361ff765 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIInstallTask.java @@ -18,7 +18,8 @@ package org.jackhuang.hmcl.download.legacyfabric; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; @@ -27,16 +28,16 @@ import java.util.Collection; import java.util.List; -public final class LegacyFabricAPIInstallTask extends Task { +public final class LegacyFabricAPIInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final LegacyFabricAPIRemoteVersion remote; private final List> dependencies = new ArrayList<>(1); - public LegacyFabricAPIInstallTask(DefaultDependencyManager dependencyManager, Version version, LegacyFabricAPIRemoteVersion remoteVersion) { + public LegacyFabricAPIInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, LegacyFabricAPIRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; } @@ -54,7 +55,7 @@ public boolean isRelyingOnDependencies() { public void execute() throws IOException { dependencies.add(new FileDownloadTask( remote.getVersion().file().url(), - dependencyManager.getGameRepository().getModsDirectory(version.getId()).resolve("legacy-fabric-api-" + remote.getVersion().version() + ".jar"), + dependencyManager.getGameRepository().getModsDirectory(manifest.id()).resolve("legacy-fabric-api-" + remote.getVersion().version() + ".jar"), remote.getVersion().file().getIntegrityCheck()) ); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java index 1ae30d48856..fbf157f14e2 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricAPIRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.task.Task; @@ -55,7 +56,7 @@ public RemoteAddon.Version getVersion() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new LegacyFabricAPIInstallTask(dependencyManager, baseVersion, this); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricInstallTask.java index f2abb9c33a6..ff0b6ef650a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricInstallTask.java @@ -24,8 +24,9 @@ import org.jackhuang.hmcl.download.fabric.FabricInstallTask; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.Artifact; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -35,17 +36,17 @@ import java.util.Collections; import java.util.List; -public final class LegacyFabricInstallTask extends Task { +public final class LegacyFabricInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final LegacyFabricRemoteVersion remote; private final GetTask launchMetaTask; private final List> dependencies = new ArrayList<>(1); - public LegacyFabricInstallTask(DefaultDependencyManager dependencyManager, Version version, LegacyFabricRemoteVersion remoteVersion) { + public LegacyFabricInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, LegacyFabricRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; launchMetaTask = new GetTask(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls())); @@ -76,10 +77,10 @@ public boolean isRelyingOnDependencies() { public void execute() { setResult(getPatch(JsonUtils.GSON.fromJson(launchMetaTask.getResult(), FabricInstallTask.FabricInfo.class), remote.getGameVersion(), remote.getSelfVersion())); - dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true)); + dependencies.add(new org.jackhuang.hmcl.download.game.GameLibrariesTask(dependencyManager, manifest, true, getResult().getLibraries())); } - private Version getPatch(FabricInstallTask.FabricInfo legacyFabricInfo, String gameVersion, String loaderVersion) { + private GameInstancePatch getPatch(FabricInstallTask.FabricInfo legacyFabricInfo, String gameVersion, String loaderVersion) { JsonObject launcherMeta = legacyFabricInfo.getLauncherMeta(); Arguments arguments = new Arguments(); @@ -110,7 +111,7 @@ private Version getPatch(FabricInstallTask.FabricInfo legacyFabricInfo, String g libraries.add(new Library(Artifact.fromDescriptor(legacyFabricInfo.getIntermediary().getMaven()), getMavenRepositoryByGroup(legacyFabricInfo.getIntermediary().getMaven()), null)); libraries.add(new Library(Artifact.fromDescriptor(legacyFabricInfo.getLoader().getMaven()), getMavenRepositoryByGroup(legacyFabricInfo.getLoader().getMaven()), null)); - return new Version(LibraryAnalyzer.LibraryType.LEGACY_FABRIC.getPatchId(), loaderVersion, Version.PRIORITY_LOADER, arguments, mainClass, libraries); + return new GameInstancePatch(LibraryAnalyzer.LibraryType.LEGACY_FABRIC.getPatchId(), loaderVersion, GameInstancePatch.PRIORITY_LOADER, arguments, mainClass, libraries); } private static String getMavenRepositoryByGroup(String maven) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java index cd29f0fddd6..f8a1e88c881 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/legacyfabric/LegacyFabricRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; import java.util.List; @@ -38,7 +39,7 @@ public class LegacyFabricRemoteVersion extends RemoteVersion { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new LegacyFabricInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java index 9d5113c2bdb..985de491c2d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderInstallTask.java @@ -33,17 +33,17 @@ * * @author huangyuhui */ -public final class LiteLoaderInstallTask extends Task { +public final class LiteLoaderInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final LiteLoaderRemoteVersion remote; private final List> dependents = new ArrayList<>(); private final List> dependencies = new ArrayList<>(1); - public LiteLoaderInstallTask(DefaultDependencyManager dependencyManager, Version version, LiteLoaderRemoteVersion remoteVersion) { + public LiteLoaderInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, LiteLoaderRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; } @@ -65,16 +65,16 @@ public void execute() { new LibrariesDownloadInfo(new LibraryDownloadInfo(null, remote.getUrls().get(0))) ); - setResult(new Version(LibraryAnalyzer.LibraryType.LITELOADER.getPatchId(), + setResult(new GameInstancePatch(LibraryAnalyzer.LibraryType.LITELOADER.getPatchId(), remote.getSelfVersion(), 60000, new Arguments().addGameArguments("--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker"), LibraryAnalyzer.LAUNCH_WRAPPER_MAIN, Lang.merge(remote.getLibraries(), Collections.singleton(library))) - .setLogging(Collections.emptyMap()) // Mods may log in malformed format, causing XML parser to crash. So we suppress using official log4j configuration + .withLogging(Collections.emptyMap()) // Mods may log in malformed format, causing XML parser to crash. So we suppress using official log4j configuration ); - dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true)); + dependencies.add(new org.jackhuang.hmcl.download.game.GameLibrariesTask(dependencyManager, manifest, true, getResult().getLibraries())); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java index ee7e05009c4..e2da5e16f84 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/liteloader/LiteLoaderRemoteVersion.java @@ -20,8 +20,9 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.Task; import java.util.Collection; @@ -54,7 +55,7 @@ public String getTweakClass() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new LiteLoaderInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeInstallTask.java index c5614c3e9d3..3467a195d37 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeInstallTask.java @@ -21,7 +21,8 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.VersionMismatchException; import org.jackhuang.hmcl.download.forge.*; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -36,10 +37,10 @@ import static org.jackhuang.hmcl.util.StringUtils.removePrefix; import static org.jackhuang.hmcl.util.StringUtils.removeSuffix; -public final class NeoForgeInstallTask extends Task { +public final class NeoForgeInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final NeoForgeRemoteVersion remoteVersion; @@ -47,11 +48,11 @@ public final class NeoForgeInstallTask extends Task { private FileDownloadTask dependent; - private Task dependency; + private Task dependency; - public NeoForgeInstallTask(DefaultDependencyManager dependencyManager, Version version, NeoForgeRemoteVersion remoteVersion) { + public NeoForgeInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, NeoForgeRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remoteVersion = remoteVersion; } @@ -96,10 +97,10 @@ public Collection> getDependencies() { @Override public void execute() throws Exception { - dependency = install(dependencyManager, version, installer); + dependency = install(dependencyManager, manifest, installer); } - public static Task install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException { + public static Task install(DefaultDependencyManager dependencyManager, GameInstanceManifest version, Path installer) throws IOException, VersionMismatchException { Optional gameVersion = dependencyManager.getGameRepository().getGameVersion(version); if (!gameVersion.isPresent()) throw new IOException(); try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { @@ -110,12 +111,12 @@ public static Task install(DefaultDependencyManager dependencyManager, if (!gameVersion.get().equals(profile.getMinecraft())) throw new VersionMismatchException(profile.getMinecraft(), gameVersion.get()); return new ForgeNewInstallTask(dependencyManager, version, modifyNeoForgeOldVersion(gameVersion.get(), profile.getVersion()), installer).thenApplyAsync(neoForgeVersion -> { - if (!neoForgeVersion.getId().equals(LibraryAnalyzer.LibraryType.FORGE.getPatchId()) || neoForgeVersion.getVersion() == null) { + if (!neoForgeVersion.id().equals(LibraryAnalyzer.LibraryType.FORGE.getPatchId()) || neoForgeVersion.version() == null) { throw new IOException("Invalid neoforge version."); } - return neoForgeVersion.setId(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId()) - .setVersion( - removePrefix(neoForgeVersion.getVersion().replace(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), ""), "-") + return neoForgeVersion.withId(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId()) + .withVersion( + removePrefix(neoForgeVersion.version().replace(LibraryAnalyzer.LibraryType.FORGE.getPatchId(), ""), "-") ); }); } else if (LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId().equals(installProfile.get("profile")) || "NeoForge".equals(installProfile.get("profile"))) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java index 2a7912aeff7..3919125371a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeOldInstallTask.java @@ -23,7 +23,7 @@ import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile; import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile.Processor; import org.jackhuang.hmcl.download.game.GameLibrariesTask; -import org.jackhuang.hmcl.download.game.VersionJsonDownloadTask; +import org.jackhuang.hmcl.download.game.GameInstanceJsonDownloadTask; import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; @@ -57,7 +57,7 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; import static org.jackhuang.hmcl.util.gson.JsonUtils.fromNonNullJson; -public class NeoForgeOldInstallTask extends Task { +public class NeoForgeOldInstallTask extends Task { private class ProcessorTask extends Task { @@ -110,7 +110,7 @@ public void execute() throws Exception { return; } - Path jar = gameRepository.getArtifactFile(version, processor.getJar()); + Path jar = gameRepository.getArtifactFile(manifest, processor.getJar()); if (!Files.isRegularFile(jar)) throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess"); @@ -128,7 +128,7 @@ public void execute() throws Exception { List classpath = new ArrayList<>(processor.getClasspath().size() + 1); for (Artifact artifact : processor.getClasspath()) { - Path file = gameRepository.getArtifactFile(version, artifact); + Path file = gameRepository.getArtifactFile(manifest, artifact); if (!Files.isRegularFile(file)) throw new Exception("Game processor dependency missing"); classpath.add(file.toString()); @@ -173,23 +173,23 @@ public void execute() throws Exception { private final DefaultDependencyManager dependencyManager; private final DefaultGameRepository gameRepository; - private final Version version; + private final GameInstanceManifest manifest; private final Path installer; private final List> dependents = new ArrayList<>(1); private final List> dependencies = new ArrayList<>(1); private ForgeNewInstallProfile profile; private List processors; - private Version neoForgeVersion; + private GameInstanceManifest neoForgeVersion; private final String selfVersion; private Path tempDir; private AtomicInteger processorDoneCount = new AtomicInteger(0); - NeoForgeOldInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) { + NeoForgeOldInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, String selfVersion, Path installer) { this.dependencyManager = dependencyManager; this.gameRepository = dependencyManager.getGameRepository(); - this.version = version; + this.manifest = manifest; this.installer = installer; this.selfVersion = selfVersion; @@ -246,7 +246,7 @@ private String parseLiteral(String literal, Map parseOptions(List args, Map vars) { @@ -333,9 +333,9 @@ private Task patchDownloadMojangMappingsTask(Processor processor, Map { - DownloadInfo mappings = fromNonNullJson(json, Version.class) + DownloadInfo mappings = fromNonNullJson(json, GameInstanceManifest.class) .getDownloads().get(DownloadType.CLIENT_MAPPINGS); if (mappings == null) { throw new Exception("client_mappings download info not found"); @@ -387,11 +387,11 @@ public void execute() throws Exception { } vars.put("SIDE", "client"); - vars.put("MINECRAFT_JAR", FileUtils.getAbsolutePath(gameRepository.getVersionJar(version))); - vars.put("MINECRAFT_VERSION", FileUtils.getAbsolutePath(gameRepository.getVersionJar(version))); + vars.put("MINECRAFT_JAR", FileUtils.getAbsolutePath(gameRepository.getInstanceJar(manifest))); + vars.put("MINECRAFT_VERSION", FileUtils.getAbsolutePath(gameRepository.getInstanceJar(manifest))); vars.put("ROOT", FileUtils.getAbsolutePath(gameRepository.getBaseDirectory())); vars.put("INSTALLER", installer.toAbsolutePath().toString()); - vars.put("LIBRARY_DIR", FileUtils.getAbsolutePath(gameRepository.getLibrariesDirectory(version))); + vars.put("LIBRARY_DIR", FileUtils.getAbsolutePath(gameRepository.getLibrariesDirectory(manifest))); updateProgress(0, processors.size()); @@ -404,10 +404,11 @@ public void execute() throws Exception { processorsTask.thenComposeAsync( dependencyManager.checkLibraryCompletionAsync(neoForgeVersion, true))); - setResult(neoForgeVersion - .setPriority(Version.PRIORITY_LOADER) - .setId(LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId()) - .setVersion(selfVersion)); + setResult(GameInstancePatch.fromManifest( + neoForgeVersion, + LibraryAnalyzer.LibraryType.NEO_FORGE.getPatchId(), + selfVersion, + GameInstancePatch.PRIORITY_LOADER)); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java index 7f6e500eac7..9582c6c7f7d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/neoforge/NeoForgeRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; import java.util.List; @@ -31,7 +32,7 @@ public NeoForgeRemoteVersion(String gameVersion, String selfVersion, List getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new NeoForgeInstallTask(dependencyManager, baseVersion, this); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java index f2a87307efe..8e39854b17c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineInstallTask.java @@ -48,11 +48,11 @@ * * @author huangyuhui */ -public final class OptiFineInstallTask extends Task { +public final class OptiFineInstallTask extends Task { private final DefaultGameRepository gameRepository; private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final OptiFineRemoteVersion remote; private final Path installer; private final List> dependents = new ArrayList<>(0); @@ -62,14 +62,14 @@ public final class OptiFineInstallTask extends Task { private final Library optiFineLibrary; private final Library optiFineInstallerLibrary; - public OptiFineInstallTask(DefaultDependencyManager dependencyManager, Version version, OptiFineRemoteVersion remoteVersion) { - this(dependencyManager, version, remoteVersion, null); + public OptiFineInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, OptiFineRemoteVersion remoteVersion) { + this(dependencyManager, manifest, remoteVersion, null); } - public OptiFineInstallTask(DefaultDependencyManager dependencyManager, Version version, OptiFineRemoteVersion remoteVersion, Path installer) { + public OptiFineInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, OptiFineRemoteVersion remoteVersion, Path installer) { this.dependencyManager = dependencyManager; this.gameRepository = dependencyManager.getGameRepository(); - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; this.installer = installer; @@ -123,14 +123,14 @@ public boolean isRelyingOnDependencies() { @Override public void execute() throws Exception { - String originalMainClass = version.resolve(dependencyManager.getGameRepository()).getMainClass(); + String originalMainClass = manifest.resolve(dependencyManager.getGameRepository()).mainClass(); if (!LibraryAnalyzer.FORGE_OPTIFINE_MAIN.contains(originalMainClass)) throw new UnsupportedInstallationException(UnsupportedInstallationException.UNSUPPORTED_LAUNCH_WRAPPER); List libraries = new ArrayList<>(4); libraries.add(optiFineLibrary); - Path optiFineInstallerLibraryPath = gameRepository.getLibraryFile(version, optiFineInstallerLibrary); + Path optiFineInstallerLibraryPath = gameRepository.getLibraryFile(manifest, optiFineInstallerLibrary); FileUtils.copyFile(dest, optiFineInstallerLibraryPath); try (FileSystem fs2 = CompressingUtils.createWritableZipFileSystem(optiFineInstallerLibraryPath)) { @@ -140,14 +140,14 @@ public void execute() throws Exception { // Install launch wrapper modified by OptiFine boolean hasLaunchWrapper = false; try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(dest)) { - Path optiFineLibraryPath = gameRepository.getLibraryFile(version, optiFineLibrary); + Path optiFineLibraryPath = gameRepository.getLibraryFile(manifest, optiFineLibrary); if (Files.exists(fs.getPath("optifine/Patcher.class"))) { String[] command = { JavaRuntime.getDefault().getBinary().toString(), "-cp", dest.toString(), "optifine.Patcher", - gameRepository.getVersionJar(version).toAbsolutePath().normalize().toString(), + gameRepository.getInstanceJar(manifest).toAbsolutePath().normalize().toString(), dest.toString(), optiFineLibraryPath.toString() }; @@ -165,7 +165,7 @@ public void execute() throws Exception { Path launchWrapper2 = fs.getPath("launchwrapper-2.0.jar"); if (Files.exists(launchWrapper2)) { Library launchWrapper = new Library(new Artifact("optifine", "launchwrapper", "2.0")); - Path launchWrapperFile = gameRepository.getLibraryFile(version, launchWrapper); + Path launchWrapperFile = gameRepository.getLibraryFile(manifest, launchWrapper); Files.createDirectories(launchWrapperFile.toAbsolutePath().getParent()); FileUtils.copyFile(launchWrapper2, launchWrapperFile); hasLaunchWrapper = true; @@ -180,7 +180,7 @@ public void execute() throws Exception { Library launchWrapper = new Library(new Artifact("optifine", "launchwrapper-of", launchWrapperVersion)); if (Files.exists(launchWrapperJar)) { - Path launchWrapperFile = gameRepository.getLibraryFile(version, launchWrapper); + Path launchWrapperFile = gameRepository.getLibraryFile(manifest, launchWrapper); Files.createDirectories(launchWrapperFile.toAbsolutePath().getParent()); FileUtils.copyFile(launchWrapperJar, launchWrapperFile); @@ -207,7 +207,7 @@ public void execute() throws Exception { libraries.add(new Library(new Artifact("net.minecraft", "launchwrapper", "1.12"))); } - setResult(new Version( + setResult(new GameInstancePatch( LibraryAnalyzer.LibraryType.OPTIFINE.getPatchId(), remote.getSelfVersion(), 10000, @@ -216,20 +216,20 @@ public void execute() throws Exception { libraries )); - dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true)); + dependencies.add(new org.jackhuang.hmcl.download.game.GameLibrariesTask(dependencyManager, manifest, true, getResult().getLibraries())); } /** * Install OptiFine library from existing local file. * * @param dependencyManager game repository - * @param version version.json + * @param version instance manifest * @param installer the OptiFine installer * @return the task to install library * @throws IOException if unable to read compressed content of installer file, or installer file is corrupted, or the installer is not the one we want. * @throws VersionMismatchException if required game version of installer does not match the actual one. */ - public static Task install(DefaultDependencyManager dependencyManager, Version version, Path installer) throws IOException, VersionMismatchException { + public static Task install(DefaultDependencyManager dependencyManager, GameInstanceManifest version, Path installer) throws IOException, VersionMismatchException { Optional gameVersion = dependencyManager.getGameRepository().getGameVersion(version); if (!gameVersion.isPresent()) throw new IOException(); try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java index 2efdf48ee52..abf36261e7a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/optifine/OptiFineRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; import java.util.List; @@ -37,7 +38,7 @@ public String getFullVersion() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new OptiFineInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIInstallTask.java index 245280e58be..1bacd55e612 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIInstallTask.java @@ -18,7 +18,8 @@ package org.jackhuang.hmcl.download.quilt; import org.jackhuang.hmcl.download.DefaultDependencyManager; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; @@ -32,16 +33,16 @@ * * @author huangyuhui */ -public final class QuiltAPIInstallTask extends Task { +public final class QuiltAPIInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final QuiltAPIRemoteVersion remote; private final List> dependencies = new ArrayList<>(1); - public QuiltAPIInstallTask(DefaultDependencyManager dependencyManager, Version version, QuiltAPIRemoteVersion remoteVersion) { + public QuiltAPIInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, QuiltAPIRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; } @@ -59,7 +60,7 @@ public boolean isRelyingOnDependencies() { public void execute() throws IOException { dependencies.add(new FileDownloadTask( remote.getVersion().file().url(), - dependencyManager.getGameRepository().getModsDirectory(version.getId()).resolve("quilt-api-" + remote.getVersion().version() + ".jar"), + dependencyManager.getGameRepository().getModsDirectory(manifest.id()).resolve("quilt-api-" + remote.getVersion().version() + ".jar"), remote.getVersion().file().getIntegrityCheck()) ); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java index bf229454a26..2ccc82a7c24 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltAPIRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.task.Task; @@ -55,7 +56,7 @@ public RemoteAddon.Version getVersion() { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new QuiltAPIInstallTask(dependencyManager, baseVersion, this); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltInstallTask.java index 01642cecf0d..31cd37672d7 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltInstallTask.java @@ -24,8 +24,9 @@ import org.jackhuang.hmcl.download.UnsupportedInstallationException; import org.jackhuang.hmcl.game.Arguments; import org.jackhuang.hmcl.game.Artifact; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonSerializable; @@ -40,17 +41,17 @@ * * @author huangyuhui */ -public final class QuiltInstallTask extends Task { +public final class QuiltInstallTask extends Task { private final DefaultDependencyManager dependencyManager; - private final Version version; + private final GameInstanceManifest manifest; private final QuiltRemoteVersion remote; private final GetTask launchMetaTask; private final List> dependencies = new ArrayList<>(1); - public QuiltInstallTask(DefaultDependencyManager dependencyManager, Version version, QuiltRemoteVersion remoteVersion) { + public QuiltInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest manifest, QuiltRemoteVersion remoteVersion) { this.dependencyManager = dependencyManager; - this.version = version; + this.manifest = manifest; this.remote = remoteVersion; launchMetaTask = new GetTask(dependencyManager.getDownloadProvider().injectURLsWithCandidates(remoteVersion.getUrls())); @@ -64,7 +65,7 @@ public boolean doPreExecute() { @Override public void preExecute() throws Exception { - if (!Objects.equals("net.minecraft.client.main.Main", version.resolve(dependencyManager.getGameRepository()).getMainClass())) + if (!Objects.equals("net.minecraft.client.main.Main", manifest.resolve(dependencyManager.getGameRepository()).mainClass())) throw new UnsupportedInstallationException(FABRIC_NOT_COMPATIBLE_WITH_FORGE); } @@ -87,10 +88,10 @@ public boolean isRelyingOnDependencies() { public void execute() { setResult(getPatch(JsonUtils.GSON.fromJson(launchMetaTask.getResult(), QuiltInfo.class), remote.getGameVersion(), remote.getSelfVersion())); - dependencies.add(dependencyManager.checkLibraryCompletionAsync(getResult(), true)); + dependencies.add(new org.jackhuang.hmcl.download.game.GameLibrariesTask(dependencyManager, manifest, true, getResult().getLibraries())); } - private Version getPatch(QuiltInfo quiltInfo, String gameVersion, String loaderVersion) { + private GameInstancePatch getPatch(QuiltInfo quiltInfo, String gameVersion, String loaderVersion) { JsonObject launcherMeta = quiltInfo.launcherMeta; Arguments arguments = new Arguments(); @@ -121,7 +122,7 @@ private Version getPatch(QuiltInfo quiltInfo, String gameVersion, String loaderV libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.intermediary.maven), getMavenRepositoryByGroup(quiltInfo.intermediary.maven), null)); libraries.add(new Library(Artifact.fromDescriptor(quiltInfo.loader.maven), getMavenRepositoryByGroup(quiltInfo.loader.maven), null)); - return new Version(LibraryAnalyzer.LibraryType.QUILT.getPatchId(), loaderVersion, Version.PRIORITY_LOADER, arguments, mainClass, libraries); + return new GameInstancePatch(LibraryAnalyzer.LibraryType.QUILT.getPatchId(), loaderVersion, GameInstancePatch.PRIORITY_LOADER, arguments, mainClass, libraries); } private static String getMavenRepositoryByGroup(String maven) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java index 979e7f794d8..bd86cd24ea3 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/download/quilt/QuiltRemoteVersion.java @@ -20,7 +20,8 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.RemoteVersion; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.task.Task; import java.util.List; @@ -38,7 +39,7 @@ public class QuiltRemoteVersion extends RemoteVersion { } @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Version baseVersion) { + public Task getInstallTask(DefaultDependencyManager dependencyManager, GameInstanceManifest baseVersion) { return new QuiltInstallTask(dependencyManager, baseVersion, this); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/GameJsonParseFailedEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/GameJsonParseFailedEvent.java index dfd1a05bdf3..b589992ad8b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/GameJsonParseFailedEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/GameJsonParseFailedEvent.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.event; +import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.util.ToStringBuilder; import java.nio.file.Path; @@ -34,7 +35,7 @@ public final class GameJsonParseFailedEvent extends Event { /** * - * @param source {@link org.jackhuang.hmcl.game.DefaultGameRepository} + * @param source {@link DefaultGameRepository} * @param jsonFile the minecraft.json file. * @param version the version name */ diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/LoadedOneVersionEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/LoadedOneVersionEvent.java deleted file mode 100644 index cedba1a2948..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/LoadedOneVersionEvent.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui 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 . - */ -package org.jackhuang.hmcl.event; - -import org.jackhuang.hmcl.game.Version; -import org.jackhuang.hmcl.util.ToStringBuilder; - -/** - * This event gets fired when a minecraft version has been loaded. - *
- * This event is fired on the {@link org.jackhuang.hmcl.event.EventBus#EVENT_BUS} - * - * @author huangyuhui - */ -public final class LoadedOneVersionEvent extends Event { - - private final Version version; - - /** - * - * @param source {@link org.jackhuang.hmcl.game.GameRepository} - * @param version the version id. - */ - public LoadedOneVersionEvent(Object source, Version version) { - super(source); - this.version = version; - } - - public Version getVersion() { - return version; - } - - @Override - public boolean hasResult() { - return true; - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .append("source", source) - .append("version", version) - .toString(); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedVersionsEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedGameInstancesEvent.java similarity index 83% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedVersionsEvent.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedGameInstancesEvent.java index 81f48f825d6..b7e6bccaa24 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedVersionsEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshedGameInstancesEvent.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.event; +import org.jackhuang.hmcl.game.GameRepository; + /** * This event gets fired when all the versions in .minecraft folder are loaded. *
@@ -24,14 +26,14 @@ * * @author huangyuhui */ -public final class RefreshedVersionsEvent extends Event { +public final class RefreshedGameInstancesEvent extends Event { /** * Constructor. * - * @param source {@link org.jackhuang.hmcl.game.GameRepository} + * @param source {@link GameRepository} */ - public RefreshedVersionsEvent(Object source) { + public RefreshedGameInstancesEvent(Object source) { super(source); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshingVersionsEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshingVersionsEvent.java index ee14df07cfe..ca2327b28d0 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshingVersionsEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RefreshingVersionsEvent.java @@ -17,6 +17,8 @@ */ package org.jackhuang.hmcl.event; +import org.jackhuang.hmcl.game.GameRepository; + /** * This event gets fired when loading versions in a .minecraft folder. *
@@ -29,7 +31,7 @@ public final class RefreshingVersionsEvent extends Event { /** * Constructor. * - * @param source {@link org.jackhuang.hmcl.game.GameRepository} + * @param source {@link GameRepository} */ public RefreshingVersionsEvent(Object source) { super(source); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RemoveVersionEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RemoveVersionEvent.java index a3ccae93e14..86ec73bacdc 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RemoveVersionEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RemoveVersionEvent.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.event; +import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.util.ToStringBuilder; /** @@ -32,7 +33,7 @@ public class RemoveVersionEvent extends Event { /** * - * @param source {@link org.jackhuang.hmcl.game.GameRepository} + * @param source {@link GameRepository} * @param version the version id. */ public RemoveVersionEvent(Object source, String version) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RenameVersionEvent.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RenameVersionEvent.java index 820404222e0..694babf651b 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RenameVersionEvent.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/event/RenameVersionEvent.java @@ -17,6 +17,7 @@ */ package org.jackhuang.hmcl.event; +import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.util.ToStringBuilder; /** @@ -32,7 +33,7 @@ public class RenameVersionEvent extends Event { /** * - * @param source {@link org.jackhuang.hmcl.game.GameRepository} + * @param source {@link GameRepository} * @param from the version id. */ public RenameVersionEvent(Object source, String from, String to) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.java deleted file mode 100644 index 17bf520673d..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/ClassicVersion.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui 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 . - */ -package org.jackhuang.hmcl.game; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.time.Instant; -import java.util.Arrays; - -/// The Minecraft version for 1.5.x and earlier. -/// -/// @author huangyuhui -public final class ClassicVersion extends Version { - - public ClassicVersion() { - super(true, "Classic", null, null, "${auth_player_name} ${auth_session} --workDir ${game_directory}", - null, "net.minecraft.client.Minecraft", null, null, null, null, null, null, - Arrays.asList(new ClassicLibrary("lwjgl"), new ClassicLibrary("jinput"), new ClassicLibrary("lwjgl_util")), - null, null, null, ReleaseType.UNKNOWN, Instant.now(), Instant.now(), 0, false, false, null); - } - - private static final class ClassicLibrary extends Library { - public ClassicLibrary(String name) { - super(new Artifact("", "", ""), null, - new LibrariesDownloadInfo(new LibraryDownloadInfo("bin/" + name + ".jar"), null), - null, null, null, null, null, null); - } - } - - public static boolean hasClassicVersion(Path baseDirectory) { - Path bin = baseDirectory.resolve("bin"); - return Files.isDirectory(bin) - && Files.exists(bin.resolve("lwjgl.jar")) - && Files.exists(bin.resolve("jinput.jar")) - && Files.exists(bin.resolve("lwjgl_util.jar")); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java index fb3f606081e..65109a89038 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/DefaultGameRepository.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui 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 @@ -18,18 +18,18 @@ package org.jackhuang.hmcl.game; import com.google.gson.JsonParseException; +import org.jackhuang.hmcl.addon.mod.ModManager; +import org.jackhuang.hmcl.addon.resourcepack.ResourcePackManager; import org.jackhuang.hmcl.download.MaintainTask; -import org.jackhuang.hmcl.download.game.VersionJsonSaveTask; import org.jackhuang.hmcl.event.*; -import org.jackhuang.hmcl.game.tlauncher.TLauncherVersion; -import org.jackhuang.hmcl.addon.mod.ModManager; import org.jackhuang.hmcl.modpack.ModpackConfiguration; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; -import org.jackhuang.hmcl.util.ToStringBuilder; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.Platform; +import org.jackhuang.hmcl.util.versioning.GameVersionNumber; +import org.jetbrains.annotations.NotNullByDefault; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -43,219 +43,381 @@ import static org.jackhuang.hmcl.util.logging.Logger.LOG; -/** - * An implementation of classic Minecraft game repository. - * - * @author huangyuhui - */ +@NotNullByDefault public class DefaultGameRepository implements GameRepository { - private Path baseDirectory; - protected Map versions; + private static final GameInstanceManifest CLASSIC_MANIFEST = new GameInstanceManifest( + new GameInstanceID("Classic"), + "${auth_player_name} ${auth_session} --workDir ${game_directory}", + null, + "net.minecraft.client.Minecraft", + null, + null, + null, + null, + null, + null, + List.of( + classicLibrary("lwjgl"), + classicLibrary("jinput"), + classicLibrary("lwjgl_util")), + null, + null, + null, + ReleaseType.UNKNOWN, + null, + null, + 0, + false, + false, + null, + null + ); + + private static Library classicLibrary(String name) { + return new Library(new Artifact("", "", ""), null, + new LibrariesDownloadInfo(new LibraryDownloadInfo("bin/" + name + ".jar"), null), + null, null, null, null, null, null); + } + + private static boolean hasClassicVersion(Path baseDirectory) { + Path bin = baseDirectory.resolve("bin"); + return Files.isDirectory(bin) + && Files.exists(bin.resolve("lwjgl.jar")) + && Files.exists(bin.resolve("jinput.jar")) + && Files.exists(bin.resolve("lwjgl_util.jar")); + } + + private volatile Status status; + private volatile boolean loaded; private final ConcurrentHashMap> gameVersions = new ConcurrentHashMap<>(); public DefaultGameRepository(Path baseDirectory) { - this.baseDirectory = baseDirectory; + this.status = new Status(baseDirectory); } public Path getBaseDirectory() { - return baseDirectory; + return status.baseDirectory; } public void setBaseDirectory(Path baseDirectory) { - this.baseDirectory = baseDirectory; + this.status = new Status(baseDirectory); + this.loaded = false; + this.gameVersions.clear(); } - @Override - public boolean hasVersion(String id) { - return id != null && versions != null && versions.containsKey(id); + public boolean isLoaded() { + return loaded; } @Override - public Version getVersion(String id) { - if (!hasVersion(id)) - throw new VersionNotFoundException("Version '" + id + "' does not exist in " + (versions == null ? "[]" : versions.keySet()) + "."); - return versions.get(id); - } + public void refresh() { + if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY) { + return; + } - @Override - public int getVersionCount() { - return versions == null ? 0 : versions.size(); + refreshImpl(); + loaded = true; + EventBus.EVENT_BUS.fireEvent(new RefreshedGameInstancesEvent(this)); } - @Override - public Collection getVersions() { - return versions == null ? Collections.emptySet() : versions.values(); - } + protected void refreshImpl() { + Status newStatus = new Status(status.baseDirectory); - @Override - public Path getLibrariesDirectory(Version version) { - return getBaseDirectory().resolve("libraries"); - } + if (hasClassicVersion(newStatus.baseDirectory)) { + GameInstanceID id = CLASSIC_MANIFEST.id(); + newStatus.instances.put(id, new InstanceHolder(newStatus, id, CLASSIC_MANIFEST)); + } - @Override - public Path getLibraryFile(Version version, Library lib) { - if ("local".equals(lib.getHint())) { - if (lib.getFileName() != null) { - return getVersionRoot(version.getId()).resolve("libraries/" + lib.getFileName()); + Path versionsDir = newStatus.baseDirectory.resolve("versions"); + if (Files.isDirectory(versionsDir)) { + try (Stream stream = Files.list(versionsDir)) { + stream.parallel().filter(Files::isDirectory).flatMap(dir -> { + GameInstanceID id; + try { + id = new GameInstanceID(FileUtils.getName(dir)); + } catch (IllegalArgumentException e) { + LOG.warning("Ignoring version folder with invalid id " + dir, e); + return Stream.empty(); + } + + Path json = dir.resolve(id + ".json"); + + if (Files.notExists(json)) { + List jsons = FileUtils.listFilesByExtension(dir, "json"); + if (jsons.size() == 1) { + LOG.info("Renaming json file " + jsons.get(0) + " to " + json); + + try { + Files.move(jsons.get(0), json); + } catch (IOException e) { + LOG.warning("Cannot rename json file, ignoring version " + id, e); + return Stream.empty(); + } + + Path jar = dir.resolve(FileUtils.getNameWithoutExtension(jsons.get(0)) + ".jar"); + if (Files.exists(jar)) { + try { + Files.move(jar, dir.resolve(id + ".jar")); + } catch (IOException e) { + LOG.warning("Cannot rename jar file, ignoring version " + id, e); + return Stream.empty(); + } + } + } else { + LOG.info("No available json file found, ignoring version " + id); + return Stream.empty(); + } + } + + GameInstanceManifest manifest; + try { + manifest = readInstanceManifest(json); + } catch (Exception e) { + LOG.warning("Malformed version json " + id, e); + if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id.id())) != Event.Result.ALLOW) { + return Stream.empty(); + } + + try { + manifest = readInstanceManifest(json); + } catch (Exception e2) { + LOG.error("User corrected version json is still malformed", e2); + return Stream.empty(); + } + } + + if (!id.equals(manifest.id())) { + try { + moveInstanceFiles(newStatus.baseDirectory, id, manifest.id()); + } catch (IOException e) { + LOG.warning("Ignoring version " + manifest.id() + + " because version id does not match folder name " + id + + ", and we cannot correct it.", e); + return Stream.empty(); + } + } + + return Stream.of(manifest); + }).forEachOrdered(it -> newStatus.instances.put( + it.id(), + new InstanceHolder(newStatus, it.id(), it))); + } catch (IOException e) { + LOG.warning("Failed to load versions from " + versionsDir, e); } + } - return getVersionRoot(version.getId()).resolve("libraries/" + lib.getArtifact().getFileName()); + Map loadedInstances = new TreeMap<>(); + for (InstanceHolder holder : newStatus.instances.values()) { + try { + GameInstanceManifest resolved = newStatus.resolve(holder.manifest, new HashSet<>()).launchManifest(); + if (CompatibilityRule.appliesToCurrentEnvironment(resolved.compatibilityRules())) { + loadedInstances.put(holder.id, holder); + } + } catch (NoSuchGameInstanceException e) { + LOG.warning("Ignoring version " + holder.id + " because it inherits from a nonexistent version."); + } } - return getLibrariesDirectory(version).resolve(lib.getPath()); + newStatus.instances.clear(); + newStatus.instances.putAll(loadedInstances); + gameVersions.clear(); + this.status = newStatus; } - public Path getArtifactFile(Version version, Artifact artifact) { - return artifact.getPath(getBaseDirectory().resolve("libraries")); + private static GameInstanceManifest readInstanceManifest(Path json) throws IOException, JsonParseException { + GameInstanceManifest manifest = JsonUtils.fromJsonFile(json, GameInstanceManifest.class); + if (manifest == null) { + throw new JsonParseException("Manifest is null"); + } + return manifest; } - @Override - public Path getRunDirectory(String id) { - return getBaseDirectory(); + private static void moveInstanceFiles(Path baseDirectory, GameInstanceID from, GameInstanceID to) throws IOException { + Path versionsDir = baseDirectory.resolve("versions"); + Path fromDir = versionsDir.resolve(from.id()); + Path toDir = versionsDir.resolve(to.id()); + Files.move(fromDir, toDir); + + Path fromJson = toDir.resolve(from + ".json"); + Path fromJar = toDir.resolve(from + ".jar"); + Path toJson = toDir.resolve(to + ".json"); + Path toJar = toDir.resolve(to + ".jar"); + + boolean hasJarFile = Files.exists(fromJar); + + try { + Files.move(fromJson, toJson); + if (hasJarFile) { + Files.move(fromJar, toJar); + } + } catch (IOException e) { + Lang.ignoringException(() -> Files.move(toJson, fromJson)); + if (hasJarFile) { + Lang.ignoringException(() -> Files.move(toJar, fromJar)); + } + Lang.ignoringException(() -> Files.move(toDir, fromDir)); + throw e; + } } @Override - public Path getVersionJar(Version version) { - Version v = version.resolve(this); - String id = Optional.ofNullable(v.getJar()).orElse(v.getId()); - return getVersionRoot(id).resolve(id + ".jar"); + public boolean hasInstance(GameInstanceID instanceId) { + return status.instances.containsKey(instanceId); } @Override - public Optional getGameVersion(Version version) { - // This implementation may cause multiple flows against the same version entering - // this function, which is accepted because GameVersion::minecraftVersion should - // be consistent. - return gameVersions.computeIfAbsent(getVersionJar(version), versionJar -> { - Optional gameVersion = GameVersion.minecraftVersion(versionJar); - if (gameVersion.isEmpty()) { - LOG.warning("Cannot find out game version of " + version.getId() + ", primary jar: " + versionJar.toString() + ", jar exists: " + Files.exists(versionJar)); - } - return gameVersion; - }); + public GameInstanceManifest getInstanceManifest(GameInstanceID instanceId) throws NoSuchGameInstanceException { + InstanceHolder instanceHolder = status.instances.get(instanceId); + if (instanceHolder == null) { + throw new NoSuchGameInstanceException(instanceId); + } + return instanceHolder.manifest; } @Override - public Path getNativeDirectory(String id, Platform platform) { - return getVersionRoot(id).resolve("natives-" + platform); + public GameInstanceManifest.Resolved getResolvedInstanceManifest(GameInstanceID instanceId) throws NoSuchGameInstanceException { + Status currentStatus = status; + + InstanceHolder instanceHolder = currentStatus.instances.get(instanceId); + if (instanceHolder == null) { + throw new NoSuchGameInstanceException(instanceId); + } + + GameInstanceManifest.Resolved resolvedManifest = instanceHolder.resolvedManifest; + if (resolvedManifest == null) { + resolvedManifest = currentStatus.resolve(instanceHolder.manifest, new HashSet<>()); + instanceHolder.resolvedManifest = resolvedManifest; + } + return resolvedManifest; } @Override - public Path getModsDirectory(String id) { - return getRunDirectory(id).resolve("mods"); + public int getInstanceCount() { + return status.instances.size(); } @Override - public Path getResourcePackDirectory(String id) { - return getRunDirectory(id).resolve("resourcepacks"); + public Path getInstanceRoot(GameInstanceID instanceId) { + return getBaseDirectory().resolve("versions").resolve(instanceId.id()); } @Override - public Path getVersionRoot(String id) { - return getBaseDirectory().resolve("versions/" + id); + public Collection getInstanceManifests() { + return status.instances.values().stream().map(i -> i.manifest).toList(); } - public Path getVersionJson(String id) { - return getVersionRoot(id).resolve(id + ".json"); + @Override + public Path getLibrariesDirectory(GameInstanceManifest manifest) { + return getBaseDirectory().resolve("libraries"); } - public Version readVersionJson(String id) throws IOException, JsonParseException { - return readVersionJson(getVersionJson(id)); - } + @Override + public Path getLibraryFile(GameInstanceManifest manifest, Library lib) { + if ("local".equals(lib.getHint())) { + if (lib.getFileName() != null) { + return getInstanceRoot(manifest.id()).resolve("libraries/" + lib.getFileName()); + } - public Version readVersionJson(Path file) throws IOException, JsonParseException { - String jsonText = Files.readString(file); - try { - // Try TLauncher version json format - return JsonUtils.fromNonNullJson(jsonText, TLauncherVersion.class).toVersion(); - } catch (JsonParseException ignored) { + return getInstanceRoot(manifest.id()).resolve("libraries/" + lib.getArtifact().getFileName()); } - try { - // Try official version json format - return JsonUtils.fromNonNullJson(jsonText, Version.class); - } catch (JsonParseException ignored) { - } + return getLibrariesDirectory(manifest).resolve(lib.getPath()); + } - LOG.warning("Cannot parse version json: " + file + "\n" + jsonText); - throw new JsonParseException("Version json incorrect"); + public Path getArtifactFile(GameInstanceManifest manifest, Artifact artifact) { + return artifact.getPath(getBaseDirectory().resolve("libraries")); } @Override - public boolean renameVersion(String from, String to) { - if (EventBus.EVENT_BUS.fireEvent(new RenameVersionEvent(this, from, to)) == Event.Result.DENY) - return false; + public Path getRunDirectory(GameInstanceID instanceId) { + return getBaseDirectory(); + } - try { - Version fromVersion = getVersion(from); - Path fromDir = getVersionRoot(from); - Path toDir = getVersionRoot(to); - Files.move(fromDir, toDir); + @Override + public Path getInstanceJar(GameInstanceID instanceId) throws NoSuchGameInstanceException { + return getInstanceJar(getResolvedInstanceManifest(instanceId).launchManifest()); + } - Path fromJson = toDir.resolve(from + ".json"); - Path fromJar = toDir.resolve(from + ".jar"); - Path toJson = toDir.resolve(to + ".json"); - Path toJar = toDir.resolve(to + ".jar"); + @Override + public Path getInstanceJar(GameInstanceManifest manifest) { + GameInstanceID jarID = Objects.requireNonNullElse(manifest.jar(), manifest.id()); + return getInstanceRoot(jarID).resolve(jarID + ".jar"); + } - boolean hasJarFile = Files.exists(fromJar); + @Override + public boolean renameInstance(GameInstanceID from, GameInstanceID to) { + if (EventBus.EVENT_BUS.fireEvent(new RenameVersionEvent(this, from.id(), to.id())) == Event.Result.DENY) { + return false; + } - try { - Files.move(fromJson, toJson); - if (hasJarFile) Files.move(fromJar, toJar); - } catch (IOException e) { - // recovery - Lang.ignoringException(() -> Files.move(toJson, fromJson)); - if (hasJarFile) Lang.ignoringException(() -> Files.move(toJar, fromJar)); - Lang.ignoringException(() -> Files.move(toDir, fromDir)); - throw e; + try { + Status currentStatus = status; + InstanceHolder fromHolder = currentStatus.instances.get(from); + if (fromHolder == null) { + throw new NoSuchGameInstanceException(from); } - if (fromVersion.getId().equals(fromVersion.getJar())) - fromVersion = fromVersion.setJar(null); - JsonUtils.writeToJsonFile(toJson, fromVersion.setId(to)); + moveInstanceFiles(currentStatus.baseDirectory, from, to); - // fix inheritsFrom of versions that inherits from version [from]. - for (Version version : getVersions()) { - if (from.equals(version.getInheritsFrom())) { - Path targetPath = getVersionJson(version.getId()); + GameInstanceManifest renamedManifest = fromHolder.manifest; + if (from.equals(renamedManifest.jar())) { + renamedManifest = renamedManifest.withJar(null); + } + renamedManifest = renamedManifest.withId(to); + JsonUtils.writeToJsonFile(getInstanceJson(to), renamedManifest); + + Map updatedInstances = new TreeMap<>(currentStatus.instances); + updatedInstances.remove(from); + updatedInstances.put(to, new InstanceHolder(currentStatus, to, renamedManifest)); + + for (InstanceHolder holder : currentStatus.instances.values()) { + GameInstanceManifest manifest = holder.manifest; + if (from.equals(manifest.inheritsFrom())) { + GameInstanceManifest updatedManifest = manifest.withInheritsFrom(to); + Path targetPath = getInstanceJson(updatedManifest.id()); Files.createDirectories(targetPath.getParent()); - JsonUtils.writeToJsonFile(targetPath, version.setInheritsFrom(to)); + JsonUtils.writeToJsonFile(targetPath, updatedManifest); + updatedInstances.put(updatedManifest.id(), new InstanceHolder(currentStatus, updatedManifest.id(), updatedManifest)); } } + + currentStatus.instances.clear(); + currentStatus.instances.putAll(updatedInstances); + gameVersions.clear(); return true; - } catch (IOException | JsonParseException | VersionNotFoundException | InvalidPathException e) { + } catch (IOException | JsonParseException | NoSuchGameInstanceException | InvalidPathException e) { LOG.warning("Unable to rename version " + from + " to " + to, e); return false; } } - public boolean removeVersionFromDisk(String id) { - if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id)) == Event.Result.DENY) + public boolean removeInstanceFromDisk(GameInstanceID id) { + if (EventBus.EVENT_BUS.fireEvent(new RemoveVersionEvent(this, id.id())) == Event.Result.DENY) { return false; - if (!versions.containsKey(id)) - return FileUtils.deleteDirectoryQuietly(getVersionRoot(id)); - Path file = getVersionRoot(id); - if (Files.notExists(file)) + } + + Status currentStatus = status; + currentStatus.instances.remove(id); + + Path file = getInstanceRoot(id); + if (Files.notExists(file)) { return true; - // test if no file in this version directory is occupied. + } + Path removedFile = file.toAbsolutePath().resolveSibling(FileUtils.getName(file) + "_removed"); try { Files.move(file, removedFile, StandardCopyOption.REPLACE_EXISTING); } catch (IOException e) { - LOG.warning("Failed to rename file " + file, e); + LOG.warning("Unable to remove version folder: " + file, e); return false; } try { - versions.remove(id); - if (FileUtils.moveToTrash(removedFile)) { return true; } - // remove json files first to ensure HMCL will not recognize this folder as a valid version. - for (Path path : FileUtils.listFilesByExtension(removedFile, "json")) { try { Files.delete(path); @@ -264,7 +426,6 @@ public boolean removeVersionFromDisk(String id) { } } - // remove the version from version list regardless of whether the directory was removed successfully or not. try { FileUtils.deleteDirectory(removedFile); } catch (IOException e) { @@ -272,194 +433,107 @@ public boolean removeVersionFromDisk(String id) { } return true; } finally { - refreshVersionsAsync().start(); + refreshAsync().start(); } } - protected void refreshVersionsImpl() { - Map versions = new TreeMap<>(); - - if (ClassicVersion.hasClassicVersion(getBaseDirectory())) { - Version version = new ClassicVersion(); - versions.put(version.getId(), version); - } - - SimpleVersionProvider provider = new SimpleVersionProvider(); - - Path versionsDir = getBaseDirectory().resolve("versions"); - if (Files.isDirectory(versionsDir)) { - try (Stream stream = Files.list(versionsDir)) { - stream.parallel().filter(Files::isDirectory).flatMap(dir -> { - String id = FileUtils.getName(dir); - Path json = dir.resolve(id + ".json"); - - // If user renamed the json file by mistake or created the json file in a wrong name, - // we will find the only json and rename it to correct name. - if (Files.notExists(json)) { - List jsons = FileUtils.listFilesByExtension(dir, "json"); - if (jsons.size() == 1) { - LOG.info("Renaming json file " + jsons.get(0) + " to " + json); - - try { - Files.move(jsons.get(0), json); - } catch (IOException e) { - LOG.warning("Cannot rename json file, ignoring version " + id, e); - return Stream.empty(); - } - - Path jar = dir.resolve(FileUtils.getNameWithoutExtension(jsons.get(0)) + ".jar"); - if (Files.exists(jar)) { - try { - Files.move(jar, dir.resolve(id + ".jar")); - } catch (IOException e) { - LOG.warning("Cannot rename jar file, ignoring version " + id, e); - return Stream.empty(); - } - } - } else { - LOG.info("No available json file found, ignoring version " + id); - return Stream.empty(); - } - } - - Version version; - try { - version = readVersionJson(json); - } catch (Exception e) { - LOG.warning("Malformed version json " + id, e); - // JsonSyntaxException or IOException or NullPointerException(!!) - if (EventBus.EVENT_BUS.fireEvent(new GameJsonParseFailedEvent(this, json, id)) != Event.Result.ALLOW) - return Stream.empty(); - - try { - version = readVersionJson(json); - } catch (Exception e2) { - LOG.error("User corrected version json is still malformed", e2); - return Stream.empty(); - } - } - - if (!id.equals(version.getId())) { - try { - String from = id; - String to = version.getId(); - Path fromDir = getVersionRoot(from); - Path toDir = getVersionRoot(to); - Files.move(fromDir, toDir); - - Path fromJson = toDir.resolve(from + ".json"); - Path fromJar = toDir.resolve(from + ".jar"); - Path toJson = toDir.resolve(to + ".json"); - Path toJar = toDir.resolve(to + ".jar"); - - try { - Files.move(fromJson, toJson); - if (Files.exists(fromJar)) - Files.move(fromJar, toJar); - } catch (IOException e) { - // recovery - Lang.ignoringException(() -> Files.move(toJson, fromJson)); - Lang.ignoringException(() -> Files.move(toJar, fromJar)); - Lang.ignoringException(() -> Files.move(toDir, fromDir)); - throw e; - } - } catch (IOException e) { - LOG.warning("Ignoring version " + version.getId() + " because version id does not match folder name " + id + ", and we cannot correct it.", e); - return Stream.empty(); - } - } - - return Stream.of(version); - }).forEachOrdered(provider::addVersion); - } catch (IOException e) { - LOG.warning("Failed to load versions from " + versionsDir, e); - } + @Override + public Optional getGameVersion(GameInstanceManifest manifest) { + try { + GameInstanceManifest resolved = resolve(manifest).launchManifest(); + Path instanceJar = getInstanceJar(resolved); + return gameVersions.computeIfAbsent(instanceJar, jar -> { + Optional gameVersion = GameVersion.minecraftVersion(jar); + if (gameVersion.isEmpty()) { + LOG.warning("Cannot find out game version of " + manifest.id() + + ", primary jar: " + jar + + ", jar exists: " + Files.exists(jar)); + } + return gameVersion; + }); + } catch (NoSuchGameInstanceException e) { + return Optional.empty(); } + } - for (Version version : provider.getVersionMap().values()) { - try { - Version resolved = version.resolve(provider); - - if (resolved.appliesToCurrentEnvironment() && - EventBus.EVENT_BUS.fireEvent(new LoadedOneVersionEvent(this, resolved)) != Event.Result.DENY) - versions.put(version.getId(), version); - } catch (VersionNotFoundException e) { - LOG.warning("Ignoring version " + version.getId() + " because it inherits from a nonexistent version."); - } - } + @Override + public Path getNativeDirectory(GameInstanceID instanceId, Platform platform) { + return getInstanceRoot(instanceId).resolve("natives-" + platform); + } - this.gameVersions.clear(); - this.versions = versions; + @Override + public Path getModsDirectory(GameInstanceID instanceId) { + return getRunDirectory(instanceId).resolve("mods"); } @Override - public void refreshVersions() { - if (EventBus.EVENT_BUS.fireEvent(new RefreshingVersionsEvent(this)) == Event.Result.DENY) - return; + public Path getResourcePackDirectory(GameInstanceID instanceId) { + return getRunDirectory(instanceId).resolve("resourcepacks"); + } - refreshVersionsImpl(); - EventBus.EVENT_BUS.fireEvent(new RefreshedVersionsEvent(this)); + public Path getInstanceJson(GameInstanceID instanceId) { + return getInstanceRoot(instanceId).resolve(instanceId.id() + ".json"); } @Override - public AssetIndex getAssetIndex(String version, String assetId) throws IOException { + public AssetIndex getAssetIndex(GameInstanceID instanceId, String assetId) throws IOException { try { - return Objects.requireNonNull(JsonUtils.fromJsonFile(getIndexFile(version, assetId), AssetIndex.class)); + return Objects.requireNonNull(JsonUtils.fromJsonFile(getIndexFile(instanceId, assetId), AssetIndex.class)); } catch (JsonParseException | NullPointerException e) { throw new IOException("Asset index file malformed", e); } } @Override - public Path getActualAssetDirectory(String version, String assetId) { + public Path getActualAssetDirectory(GameInstanceID instanceId, String assetId) { try { - return reconstructAssets(version, assetId); + return reconstructAssets(instanceId, assetId); } catch (IOException | JsonParseException e) { LOG.error("Unable to reconstruct asset directory", e); - return getAssetDirectory(version, assetId); + return getAssetDirectory(instanceId, assetId); } } @Override - public Path getAssetDirectory(String version, String assetId) { + public Path getAssetDirectory(GameInstanceID instanceId, String assetId) { return getBaseDirectory().resolve("assets"); } @Override - public Optional getAssetObject(String version, String assetId, String name) throws IOException { + public Optional getAssetObject(GameInstanceID instanceId, String assetId, String name) throws IOException { try { - AssetObject assetObject = getAssetIndex(version, assetId).getObjects().get(name); + AssetObject assetObject = getAssetIndex(instanceId, assetId).getObjects().get(name); if (assetObject == null) return Optional.empty(); - return Optional.of(getAssetObject(version, assetId, assetObject)); + return Optional.of(getAssetObject(instanceId, assetId, assetObject)); } catch (IOException e) { throw e; } catch (Exception e) { - throw new IOException("Unrecognized asset object " + name + " in asset " + assetId + " of version " + version, e); + throw new IOException("Unrecognized asset object " + name + " in asset " + assetId + " of version " + instanceId, e); } } @Override - public Path getAssetObject(String version, String assetId, AssetObject obj) { - return getAssetObject(version, getAssetDirectory(version, assetId), obj); + public Path getAssetObject(GameInstanceID instanceId, String assetId, AssetObject obj) { + return getAssetObject(instanceId, getAssetDirectory(instanceId, assetId), obj); } - public Path getAssetObject(String version, Path assetDir, AssetObject obj) { + public Path getAssetObject(GameInstanceID instanceId, Path assetDir, AssetObject obj) { return assetDir.resolve("objects").resolve(obj.getLocation()); } @Override - public Path getIndexFile(String version, String assetId) { - return getAssetDirectory(version, assetId).resolve("indexes").resolve(assetId + ".json"); + public Path getIndexFile(GameInstanceID instanceId, String assetId) { + return getAssetDirectory(instanceId, assetId).resolve("indexes").resolve(assetId + ".json"); } @Override - public Path getLoggingObject(String version, String assetId, LoggingInfo loggingInfo) { - return getAssetDirectory(version, assetId).resolve("log_configs").resolve(loggingInfo.file().getId()); + public Path getLoggingObject(GameInstanceID instanceId, String assetId, LoggingInfo loggingInfo) { + return getAssetDirectory(instanceId, assetId).resolve("log_configs").resolve(loggingInfo.file().getId()); } - protected Path reconstructAssets(String version, String assetId) throws IOException, JsonParseException { - Path assetsDir = getAssetDirectory(version, assetId); - Path indexFile = getIndexFile(version, assetId); + protected Path reconstructAssets(GameInstanceID instanceId, String assetId) throws IOException, JsonParseException { + Path assetsDir = getAssetDirectory(instanceId, assetId); + Path indexFile = getIndexFile(instanceId, assetId); Path virtualRoot = assetsDir.resolve("virtual").resolve(assetId); if (!Files.isRegularFile(indexFile)) @@ -471,13 +545,13 @@ protected Path reconstructAssets(String version, String assetId) throws IOExcept return assetsDir; if (index.isVirtual()) { - Path resourcesDir = getRunDirectory(version).resolve("resources"); + Path resourcesDir = getRunDirectory(instanceId).resolve("resources"); int cnt = 0; int tot = index.getObjects().size(); for (Map.Entry entry : index.getObjects().entrySet()) { Path target = virtualRoot.resolve(entry.getKey()); - Path original = getAssetObject(version, assetsDir, entry.getValue()); + Path original = getAssetObject(instanceId, assetsDir, entry.getValue()); if (Files.exists(original)) { cnt++; if (!Files.isRegularFile(target)) @@ -491,7 +565,7 @@ protected Path reconstructAssets(String version, String assetId) throws IOExcept } } - // If the scale new format existent file is lower then 0.1, use the old format. + // If the scale new format existent file is lower than 0.1, use the old format. if (cnt * 10 < tot) return assetsDir; else @@ -501,64 +575,187 @@ protected Path reconstructAssets(String version, String assetId) throws IOExcept return assetsDir; } - public Task saveAsync(Version version) { - this.gameVersions.remove(getVersionJar(version)); - if (version.isResolvedPreservingPatches()) { - return new VersionJsonSaveTask(this, MaintainTask.maintainPreservingPatches(this, version)); - } else { - return new VersionJsonSaveTask(this, version); - } - } + public Task saveAsync(GameInstanceManifest instanceManifest) { + return Task.supplyAsync(() -> { + GameInstanceManifest savedManifest = instanceManifest.isResolvedPreservingPatches() + ? MaintainTask.maintainPreservingPatches(this, instanceManifest) + : instanceManifest; - public boolean isLoaded() { - return versions != null; + Path json = getInstanceJson(savedManifest.id()).toAbsolutePath(); + Files.createDirectories(json.getParent()); + JsonUtils.writeToJsonFile(json, savedManifest); + + Status currentStatus = status; + currentStatus.instances.put(savedManifest.id(), new InstanceHolder(currentStatus, savedManifest.id(), savedManifest)); + gameVersions.clear(); + return savedManifest; + }); } - public Path getModpackConfiguration(String version) { - return getVersionRoot(version).resolve("modpack.json"); + public Path getModpackConfiguration(GameInstanceID instanceId) { + return getInstanceRoot(instanceId).resolve("modpack.json"); } - /** - * read modpack configuration for a version. - * - * @param version version installed as modpack - * @return modpack configuration object, or null if this version is not a modpack. - * @throws VersionNotFoundException if version does not exist. - * @throws IOException if an i/o error occurs. - */ @Nullable - public ModpackConfiguration readModpackConfiguration(String version) throws IOException, VersionNotFoundException { - if (!hasVersion(version)) throw new VersionNotFoundException(version); - Path file = getModpackConfiguration(version); + public ModpackConfiguration readModpackConfiguration(GameInstanceID instanceId) throws IOException, NoSuchGameInstanceException { + if (!hasInstance(instanceId)) throw new NoSuchGameInstanceException(instanceId); + Path file = getModpackConfiguration(instanceId); if (Files.notExists(file)) return null; return JsonUtils.fromJsonFile(file, ModpackConfiguration.class); } - public boolean isModpack(String version) { - return Files.exists(getModpackConfiguration(version)); + public boolean isModpack(GameInstanceID instanceId) { + return Files.exists(getModpackConfiguration(instanceId)); + } + + public Path getSavesDirectory(GameInstanceID instanceId) { + return getRunDirectory(instanceId).resolve("saves"); } - public ModManager getModManager(String version) { - return new ModManager(this, version); + public Path getBackupsDirectory(GameInstanceID instanceID) { + return getRunDirectory(instanceID).resolve("backups"); } - public Path getSavesDirectory(String id) { - return getRunDirectory(id).resolve("saves"); + public Path getSchematicsDirectory(GameInstanceID instanceId) { + return getRunDirectory(instanceId).resolve("schematics"); } - public Path getBackupsDirectory(String id) { - return getRunDirectory(id).resolve("backups"); + public ModManager getModManager(GameInstanceID instanceId) { + return new ModManager(this, instanceId); } - public Path getSchematicsDirectory(String id) { - return getRunDirectory(id).resolve("schematics"); + public ResourcePackManager getResourcePackManager(GameInstanceID instanceId) { + return new ResourcePackManager(this, instanceId); } @Override - public String toString() { - return new ToStringBuilder(this) - .append("versions", versions == null ? null : versions.keySet()) - .append("baseDirectory", baseDirectory) - .toString(); + public GameInstanceManifest.Resolved resolve(GameInstanceManifest manifest) throws NoSuchGameInstanceException { + return status.resolve(manifest, new HashSet<>()); + } + + protected static class Status { + private final Path baseDirectory; + private final Map instances = new TreeMap<>(); + + protected Status(Path baseDirectory) { + this.baseDirectory = baseDirectory; + } + + private List getInheritanceChain(GameInstanceManifest manifest) throws NoSuchGameInstanceException { + if (manifest.inheritsFrom() == null) + return List.of(manifest); + + List chain = new ArrayList<>(2); + + for (GameInstanceManifest currentManifest = manifest; ; ) { + chain.add(currentManifest); + if (currentManifest.inheritsFrom() == null) { + break; + } else { + InstanceHolder parent = instances.get(currentManifest.inheritsFrom()); + if (parent == null) { + throw new NoSuchGameInstanceException(currentManifest.inheritsFrom()); + } + currentManifest = parent.manifest; + } + } + + return chain; + } + + private GameInstanceManifest.Resolved resolve(GameInstanceManifest manifest, + Set resolvedSoFar) throws NoSuchGameInstanceException { + GameInstanceManifest launchManifest; + GameInstanceManifest standaloneManifest = manifest.isRoot() + ? manifest + : addPatches( + addPatches(new GameInstanceManifest(manifest.id()), List.of(manifest.toPatch())), + manifest.patches()); + + if (manifest.inheritsFrom() == null) { + if (manifest.isRoot()) { + // TODO: Breaking change, require much testing on versions installed with external installer, other launchers, and all kinds of versions. + launchManifest = manifest.patches() != null ? new GameInstanceManifest(manifest.id()).withPatches(manifest.patches()) : manifest; + } else { + launchManifest = manifest; + } + launchManifest = launchManifest.withJar(manifest.jar() == null ? manifest.id() : manifest.jar()); + } else { + // To maximize the compatibility. + if (!resolvedSoFar.add(manifest.id())) { + LOG.warning("Found circular dependency versions: " + resolvedSoFar); + launchManifest = manifest.jar() == null ? manifest.withJar(manifest.id()) : manifest; + } else { + InstanceHolder parentInstance = instances.get(manifest.inheritsFrom()); + if (parentInstance == null) { + throw new NoSuchGameInstanceException(manifest.inheritsFrom()); + } + + // It is supposed to auto-install a version in getVersion. + GameInstanceManifest.Resolved parentResolved = resolve(parentInstance.manifest, resolvedSoFar); + launchManifest = manifest.merge(parentResolved.launchManifest()); + standaloneManifest = addPatches( + addPatches(parentResolved.standaloneManifest(), Collections.singleton(manifest.toPatch())), + manifest.patches()); + } + } + + if (manifest.patches() != null && !manifest.patches().isEmpty()) { + // Assume patches themselves do not have patches recursively. + List sortedPatches = manifest.patches().stream() + .sorted(Comparator.comparing(GameInstancePatch::getPriority)) + .toList(); + for (GameInstancePatch patch : sortedPatches) { + launchManifest = patch.merge(launchManifest); + } + } + + launchManifest = launchManifest.withId(manifest.id()).withPatches(null); + standaloneManifest = standaloneManifest.withId(manifest.id()); + if (launchManifest.jar() != null) { + standaloneManifest = standaloneManifest.withJar(launchManifest.jar()); + } + + return new GameInstanceManifest.Resolved(manifest, launchManifest, standaloneManifest); + } + + private static GameInstanceManifest addPatches(GameInstanceManifest manifest, @Nullable Collection additional) { + if (additional == null || additional.isEmpty()) { + return manifest; + } + + Set patchIds = new HashSet<>(); + for (GameInstancePatch patch : additional) { + if (patch.id() != null) { + patchIds.add(patch.id()); + } + } + + List patches = new ArrayList<>(); + if (manifest.patches() != null) { + for (GameInstancePatch patch : manifest.patches()) { + if (patch.id() == null || !patchIds.contains(patch.id())) { + patches.add(patch); + } + } + } + patches.addAll(additional); + return manifest.withPatches(patches); + } + + } + + protected static class InstanceHolder { + protected final Status status; + protected final GameInstanceID id; + protected final GameInstanceManifest manifest; + protected @Nullable GameInstanceManifest.Resolved resolvedManifest; + protected @Nullable GameVersionNumber version; + + protected InstanceHolder(Status status, GameInstanceID id, GameInstanceManifest manifest) { + this.status = status; + this.id = id; + this.manifest = manifest; + } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceID.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceID.java new file mode 100644 index 00000000000..2b93994ee97 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceID.java @@ -0,0 +1,68 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.game; + +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import org.jackhuang.hmcl.util.gson.JsonSerializable; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; + +@NotNullByDefault +@JsonAdapter(GameInstanceID.Adapter.class) +@JsonSerializable +public record GameInstanceID(String id) implements Comparable { + public GameInstanceID { + if (id.isEmpty()) { + throw new IllegalArgumentException("Game instance id cannot be empty"); + } + } + + @Override + public int compareTo(GameInstanceID that) { + return this.id.compareTo(that.id); + } + + @Override + public String toString() { + return id; + } + + static final class Adapter extends TypeAdapter<@Nullable GameInstanceID> { + + @Override + public @Nullable GameInstanceID read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + return new GameInstanceID(in.nextString()); + } + + @Override + public void write(JsonWriter out, @Nullable GameInstanceID value) throws IOException { + out.value(value != null ? value.id() : null); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceLibraryBuilder.java similarity index 88% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceLibraryBuilder.java index 113a3ebb446..60c6ebbbb13 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionLibraryBuilder.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceLibraryBuilder.java @@ -20,18 +20,15 @@ import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.platform.CommandBuilder; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; /** * * @author huangyuhui */ -public final class VersionLibraryBuilder { - private final Version version; +public final class GameInstanceLibraryBuilder { + private final GameInstanceManifest manifest; private final List mcArgs; private final List game; private final List jvm; @@ -39,32 +36,32 @@ public final class VersionLibraryBuilder { private final boolean useMcArgs; private boolean jvmChanged = false; - public VersionLibraryBuilder(Version version) { - this.version = version; - this.libraries = new ArrayList<>(version.getLibraries()); - this.mcArgs = version.getMinecraftArguments().map(StringUtils::tokenize).map(ArrayList::new).orElse(null); - this.game = version.getArguments().map(Arguments::game).map(ArrayList::new).orElseGet(ArrayList::new); - this.jvm = new ArrayList<>(version.getArguments().map(Arguments::jvm).orElse(Arguments.DEFAULT_JVM_ARGUMENTS)); + public GameInstanceLibraryBuilder(GameInstanceManifest manifest) { + this.manifest = manifest; + this.libraries = new ArrayList<>(manifest.getLibraries()); + this.mcArgs = Optional.ofNullable(manifest.minecraftArguments()).map(StringUtils::tokenize).map(ArrayList::new).orElse(null); + this.game = Optional.ofNullable(manifest.arguments()).map(Arguments::game).map(ArrayList::new).orElseGet(ArrayList::new); + this.jvm = new ArrayList<>(Optional.ofNullable(manifest.arguments()).map(Arguments::jvm).orElse(Arguments.DEFAULT_JVM_ARGUMENTS)); this.useMcArgs = mcArgs != null; } - public Version build() { - Version ret = version; + public GameInstanceManifest build() { + GameInstanceManifest ret = manifest; if (useMcArgs) { // The official launcher will not parse the "arguments" property when it detects the presence of "mcArgs". // The "arguments" property with the "rule" is simply ignored here. this.mcArgs.addAll(this.game.stream().map(arg -> arg.toString(new HashMap<>(), new HashMap<>())).flatMap(Collection::stream).collect(Collectors.toList())); - ret = ret.setArguments(null); + ret = ret.withArguments(null); // Since $ will be escaped in linux, and our maintain of minecraftArgument will not cause escaping, // so we regenerate the minecraftArgument without escaping. - ret = ret.setMinecraftArguments(new CommandBuilder().addAllWithoutParsing(mcArgs).toString()); + ret = ret.withMinecraftArguments(new CommandBuilder().addAllWithoutParsing(mcArgs).toString()); } else { - ret = ret.setArguments(ret.getArguments() + ret = ret.withArguments(Optional.ofNullable(ret.arguments()) .map(args -> args.withGame(game)) .map(args -> jvmChanged ? args.withJvm(jvm) : args).orElse(new Arguments(game, jvmChanged ? jvm : null))); } - return ret.setLibraries(libraries); + return ret.withLibraries(libraries); } public boolean hasTweakClass(String tweakClass) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java new file mode 100644 index 00000000000..3fb6e8d773c --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstanceManifest.java @@ -0,0 +1,966 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.game; + +import com.google.gson.*; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import org.jackhuang.hmcl.util.Constants; +import org.jackhuang.hmcl.util.ImmutableSequencedMap; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.gson.InstantTypeAdapter; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jackhuang.hmcl.util.gson.LowerCaseEnumTypeAdapter; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.io.IOException; +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.*; + +@NotNullByDefault +@JsonAdapter(GameInstanceManifest.Adapter.class) +public record GameInstanceManifest( + GameInstanceID id, + @Nullable String minecraftArguments, + @Nullable Arguments arguments, + @Nullable String mainClass, + @Nullable GameInstanceID inheritsFrom, + @Nullable GameInstanceID jar, + @Nullable AssetIndexInfo assetIndex, + @Nullable String assets, + @Nullable Integer complianceLevel, + @Nullable GameJavaVersion javaVersion, + @Nullable @Unmodifiable List libraries, + @Nullable @Unmodifiable List compatibilityRules, + @Nullable @Unmodifiable Map downloads, + @Nullable @Unmodifiable Map logging, + @Nullable ReleaseType type, + @Nullable Instant time, + @Nullable Instant releaseTime, + @Nullable Integer minimumLauncherVersion, + @Nullable Boolean root, + @Nullable Boolean hidden, + @Nullable @Unmodifiable List patches, + @Nullable @Unmodifiable JsonObject rawJson +) { + + /// Resolved manifest views with inheritance folded. + /// + /// @param launchManifest the final manifest data used by launch-time consumers + /// @param standaloneManifest the standalone manifest data with pending patches preserved + @NotNullByDefault + public record Resolved(GameInstanceManifest unresolved, + GameInstanceManifest launchManifest, + GameInstanceManifest standaloneManifest) { + + /// Creates a resolved manifest view. + public Resolved { + Objects.requireNonNull(launchManifest); + Objects.requireNonNull(standaloneManifest); + + if (!launchManifest.id().equals(standaloneManifest.id())) { + throw new IllegalArgumentException("Resolved manifest views must have the same id"); + } + + if (launchManifest.inheritsFrom() != null) { + throw new IllegalArgumentException("Launch manifest cannot inherit from another manifest"); + } + if (launchManifest.patches() != null && !launchManifest.patches().isEmpty()) { + throw new IllegalArgumentException("Launch manifest cannot contain pending patches"); + } + if (standaloneManifest.inheritsFrom() != null) { + throw new IllegalArgumentException("Standalone manifest cannot inherit from another manifest"); + } + } + } + + GameInstanceManifest merge(GameInstanceManifest parent) { + return new GameInstanceManifest( + id, + minecraftArguments == null ? parent.minecraftArguments : minecraftArguments, + Arguments.merge(parent.arguments, arguments), + mainClass == null ? parent.mainClass : mainClass, + null, // inheritsFrom + jar == null ? parent.jar : jar, + assetIndex == null ? parent.assetIndex : assetIndex, + assets == null ? parent.assets : assets, + complianceLevel, + javaVersion == null ? parent.javaVersion : javaVersion, + Lang.merge(this.libraries, parent.libraries), + Lang.merge(parent.compatibilityRules, this.compatibilityRules), + downloads == null ? parent.downloads : downloads, + logging == null ? parent.logging : logging, + type == null ? parent.type : type, + time == null ? parent.time : time, + releaseTime == null ? parent.releaseTime : releaseTime, + Lang.merge(minimumLauncherVersion, parent.minimumLauncherVersion, Math::max), + true, + hidden, + Lang.merge(Lang.merge(parent.patches, Collections.singleton(toPatch())), patches), + null + ); + } + + public GameInstanceManifest(GameInstanceID id) { + this(id, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + true, + false, + null, + null); + } + + public static GameInstanceManifest fromJson(JsonObject json, boolean copyJson) throws JsonParseException { + if (copyJson) { + json = json.deepCopy(); + } + + Builder builder = new Builder(); + + for (Map.Entry entry : json.entrySet()) { + String memberName = entry.getKey(); + JsonElement value = entry.getValue(); + + switch (memberName) { + case "id" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + builder.id = new GameInstanceID(primitive.getAsString()); + } catch (IllegalArgumentException e) { + throw new JsonParseException(e); + } + } + } + case "minecraftArguments" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + builder.minecraftArguments = primitive.getAsString(); + } + } + case "arguments" -> { + builder.arguments = JsonUtils.GSON.fromJson(value, Arguments.class); + } + case "mainClass" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + builder.mainClass = primitive.getAsString(); + } + } + case "inheritsFrom" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + builder.inheritsFrom = new GameInstanceID(primitive.getAsString()); + } catch (IllegalArgumentException ignored) { + } + } + } + case "jar" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + builder.jar = new GameInstanceID(primitive.getAsString()); + } catch (IllegalArgumentException ignored) { + } + } + } + case "assetIndex" -> { + builder.assetIndex = JsonUtils.GSON.fromJson(value, AssetIndexInfo.class); + } + case "assets" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + builder.assets = primitive.getAsString(); + } + } + case "complianceLevel" -> { + if (value instanceof JsonPrimitive primitive && primitive.isNumber()) { + builder.complianceLevel = primitive.getAsInt(); + } + } + case "javaVersion" -> { + builder.javaVersion = JsonUtils.GSON.fromJson(value, GameJavaVersion.class); + } + case "libraries" -> { + if (value instanceof JsonArray array) { + List list = new ArrayList<>(array.size()); + for (JsonElement element : array) { + if (element instanceof JsonObject object) { + list.add(Library.fromJson(object)); + } else { + throw new JsonParseException("Invalid library element: " + element); + } + } + builder.libraries = List.copyOf(list); + } + } + case "compatibilityRules" -> { + if (value instanceof JsonArray array) { + List list = new ArrayList<>(array.size()); + for (JsonElement element : array) { + list.add(JsonUtils.GSON.fromJson(element, CompatibilityRule.class)); + } + builder.compatibilityRules = List.copyOf(list); + } + } + case "downloads" -> { + if (value instanceof JsonObject obj) { + var map = new LinkedHashMap(); + for (Map.Entry downloadEntry : obj.entrySet()) { + try { + DownloadType key = LowerCaseEnumTypeAdapter.fromJson(DownloadType.class, downloadEntry.getKey()); + if (key != null) { + map.put(key, JsonUtils.GSON.fromJson(downloadEntry.getValue(), DownloadInfo.class)); + } + } catch (IllegalArgumentException ignored) { + } + } + builder.downloads = ImmutableSequencedMap.wrap(map); + } + } + case "logging" -> { + if (value instanceof JsonObject obj) { + var map = new LinkedHashMap(); + for (Map.Entry loggingEntry : obj.entrySet()) { + try { + DownloadType key = LowerCaseEnumTypeAdapter.fromJson(DownloadType.class, loggingEntry.getKey()); + if (key != null) { + map.put(key, JsonUtils.GSON.fromJson(loggingEntry.getValue(), LoggingInfo.class)); + } + } catch (IllegalArgumentException ignored) { + } + } + builder.logging = ImmutableSequencedMap.wrap(map); + } + } + case "type" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + builder.type = LowerCaseEnumTypeAdapter.fromJson(ReleaseType.class, primitive.getAsString()); + } catch (IllegalArgumentException ignored) { + } + } + } + case "time" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + builder.time = InstantTypeAdapter.deserializeToInstant(primitive.getAsString()); + } catch (Exception ignored) { + } + } + } + case "releaseTime" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + builder.releaseTime = InstantTypeAdapter.deserializeToInstant(primitive.getAsString()); + } catch (Exception ignored) { + } + } + } + case "minimumLauncherVersion" -> { + if (value instanceof JsonPrimitive primitive && primitive.isNumber()) { + builder.minimumLauncherVersion = primitive.getAsInt(); + } + } + case "root" -> { + if (value instanceof JsonPrimitive primitive && primitive.isBoolean()) { + builder.root = primitive.getAsBoolean(); + } + } + case "hidden" -> { + if (value instanceof JsonPrimitive primitive && primitive.isBoolean()) { + builder.hidden = primitive.getAsBoolean(); + } + } + case "patches" -> { + if (value instanceof JsonArray array) { + List list = new ArrayList<>(array.size()); + for (JsonElement element : array) { + if (element instanceof JsonObject object) { + list.add(GameInstancePatch.fromJson(object)); + } + } + builder.patches = List.copyOf(list); + } + } + } + } + + builder.rawJson = json; + if (builder.id == null) { + throw new JsonParseException("Missing instance id"); + } + + return builder.toManifest(); + } + + /// Returns whether the manifest is hidden. + /// + /// @return whether the manifest is hidden + public boolean isHidden() { + return hidden != null && hidden; + } + + /// Returns whether the manifest is a root manifest. + /// + /// @return whether the manifest is a root manifest + public boolean isRoot() { + return root != null && root; + } + + /// Returns whether this manifest is already a standalone view. + /// + /// @return whether this manifest has no parent + public boolean isResolvedPreservingPatches() { + return inheritsFrom == null; + } + + /// Returns the pending patches. + /// + /// @return the pending patches, or an empty list when absent + public List getPatches() { + return patches == null ? List.of() : patches; + } + + /// Returns logging metadata. + /// + /// @return logging metadata + public Map getLogging() { + return logging == null ? Map.of() : logging; + } + + /// Returns libraries. + /// + /// @return libraries + public List getLibraries() { + return libraries == null ? List.of() : libraries; + } + + /// Returns download metadata. + /// + /// @return download metadata + public Map getDownloads() { + return downloads == null ? Map.of() : downloads; + } + + /// Returns client jar download information. + /// + /// @return client jar download information + public DownloadInfo getDownloadInfo() { + DownloadInfo client = downloads == null ? null : downloads.get(DownloadType.CLIENT); + if (client != null) { + return client; + } else { + GameInstanceID jarInstanceId = Objects.requireNonNullElse(jar, id); + return new DownloadInfo(String.format("%s%s/%s.jar", Constants.DEFAULT_VERSION_DOWNLOAD_URL, jarInstanceId, jarInstanceId)); + } + } + + /// Returns the asset index metadata. + /// + /// @return asset index metadata + public AssetIndexInfo getAssetIndex() { + String assetsId = assets == null ? "legacy" : assets; + + if (assetIndex == null) { + String hash; + switch (assetsId) { + case "1.8" -> hash = "f6ad102bcaa53b1a58358f16e376d548d44933ec"; + case "14w31a" -> hash = "10a2a0e75b03cfb5a7196abbdf43b54f7fa61deb"; + case "14w25a" -> hash = "32ff354a3be1c4dd83027111e6d79ee4d701d2c0"; + case "1.7.4" -> hash = "545510a60f526b9aa8a38f9c0bc7a74235d21675"; + case "1.7.10" -> hash = "1863782e33ce7b584fc45b037325a1964e095d3e"; + case "1.7.3" -> hash = "f6cf726f4747128d13887010c2cbc44ba83504d9"; + case "pre-1.6" -> hash = "3d8e55480977e32acd9844e545177e69a52f594b"; + case "legacy" -> hash = "770572e819335b6c0a053f8378ad88eda189fc14"; + default -> { + assetsId = "legacy"; + hash = "770572e819335b6c0a053f8378ad88eda189fc14"; + } + } + + String url = Constants.DEFAULT_INDEX_URL + hash + "/" + assetsId + ".json"; + return new AssetIndexInfo(assetsId, url); + } else { + return assetIndex; + } + } + + /// Returns whether this manifest applies to the current environment. + /// + /// @return whether this manifest applies to the current environment + public boolean appliesToCurrentEnvironment() { + return CompatibilityRule.appliesToCurrentEnvironment(compatibilityRules); + } + + /// Resolves this manifest through the repository. + /// + /// @param repository the repository that provides parent manifests + /// @return the resolved manifest + public GameInstanceManifest resolve(GameRepository repository) throws NoSuchGameInstanceException { + return repository.resolve(this).launchManifest(); + } + + public GameInstanceManifest withId(GameInstanceID id) { + Objects.requireNonNull(id); + + if (Objects.equals(this.id, id)) { + return this; + } + + Builder builder = new Builder(this); + builder.setId(id); + return builder.toManifest(); + } + + public GameInstanceManifest withJar(@Nullable GameInstanceID jar) { + if (Objects.equals(this.jar, jar)) { + return this; + } + + Builder builder = new Builder(this); + builder.setJar(jar); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given parent instance id. + /// + /// @param inheritsFrom the parent instance id, or `null` if this manifest is independent + /// @return a manifest with the requested parent instance id + public GameInstanceManifest withInheritsFrom(@Nullable GameInstanceID inheritsFrom) { + if (Objects.equals(this.inheritsFrom, inheritsFrom)) { + return this; + } + + Builder builder = new Builder(this); + builder.setInheritsFrom(inheritsFrom); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given legacy game arguments. + public GameInstanceManifest withMinecraftArguments(@Nullable String minecraftArguments) { + if (Objects.equals(this.minecraftArguments, minecraftArguments)) { + return this; + } + + Builder builder = new Builder(this); + builder.setMinecraftArguments(minecraftArguments); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given structured arguments. + public GameInstanceManifest withArguments(@Nullable Arguments arguments) { + if (this.arguments == arguments) + return this; + + Builder builder = new Builder(this); + builder.setArguments(arguments); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given main class. + public GameInstanceManifest withMainClass(@Nullable String mainClass) { + if (Objects.equals(this.mainClass, mainClass)) + return this; + + Builder builder = new Builder(this); + builder.setMainClass(mainClass); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given asset index. + public GameInstanceManifest withAssetIndex(@Nullable AssetIndexInfo assetIndex) { + if (this.assetIndex == assetIndex) + return this; + + Builder builder = new Builder(this); + builder.setAssetIndex(assetIndex); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given Java version. + public GameInstanceManifest withJavaVersion(@Nullable GameJavaVersion javaVersion) { + if (this.javaVersion == javaVersion) + return this; + + Builder builder = new Builder(this); + builder.setJavaVersion(javaVersion); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given libraries. + public GameInstanceManifest withLibraries(@Nullable List libraries) { + if (this.libraries == libraries) + return this; + + Builder builder = new Builder(this); + builder.setLibraries(libraries); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given downloads. + public GameInstanceManifest withDownloads(@Nullable Map downloads) { + if (this.downloads == downloads) + return this; + + Builder builder = new Builder(this); + builder.setDownloads(downloads); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given logging metadata. + public GameInstanceManifest withLogging(@Nullable Map logging) { + if (this.logging == logging) + return this; + + Builder builder = new Builder(this); + builder.setLogging(logging); + return builder.toManifest(); + } + + /// Returns a manifest copy with the given root flag. + public GameInstanceManifest withRoot(@Nullable Boolean root) { + if (Objects.equals(this.root, root)) + return this; + + Builder builder = new Builder(this); + builder.setRoot(root); + return builder.toManifest(); + } + + public GameInstanceManifest withPatches(@Nullable List patches) { + if (patches == this.patches) { + return this; + } + + Builder builder = new Builder(this); + builder.setPatches(patches); + return builder.toManifest(); + } + + /// Returns a manifest copy with additional patches. + public GameInstanceManifest addPatch(GameInstancePatch additional) { + return addPatches(List.of(additional)); + } + + /// Returns a manifest copy with additional patches. + public GameInstanceManifest addPatches(@Nullable List additional) { + Set patchIds = new HashSet<>(); + if (additional != null) { + for (GameInstancePatch patch : additional) { + if (patch.id() != null) { + patchIds.add(patch.id()); + } + } + } + + List patches = new ArrayList<>(); + if (this.patches != null) { + for (GameInstancePatch patch : this.patches) { + if (patch.id() == null || !patchIds.contains(patch.id())) { + patches.add(patch); + } + } + } + if (additional != null) { + patches.addAll(additional); + } + return withPatches(patches); + } + + /// Returns whether this manifest has a patch with the given id. + public boolean hasPatch(String patchId) { + return patches != null && patches.stream().anyMatch(patch -> patchId.equals(patch.id())); + } + + /// Converts this manifest into a hidden patch entry for preserving resolved inheritance. + /// + /// @return a patch containing this manifest's launch metadata + public GameInstancePatch toPatch() { + return new GameInstancePatch( + "resolved." + id, + null, + Integer.MIN_VALUE, + minecraftArguments, + arguments, + mainClass, + inheritsFrom, + jar, + assetIndex, + assets, + complianceLevel, + javaVersion, + libraries, + compatibilityRules, + downloads, + logging, + type, + time, + releaseTime, + minimumLauncherVersion, + true, + null); + } + + public JsonObject toJsonObject() { + if (rawJson != null) { + return rawJson.deepCopy(); + } + + JsonObject json = new JsonObject(); + json.addProperty("id", id.toString()); + + if (minecraftArguments != null) + json.addProperty("minecraftArguments", minecraftArguments); + if (arguments != null) + json.add("arguments", JsonUtils.GSON.toJsonTree(arguments)); + if (mainClass != null) + json.addProperty("mainClass", mainClass); + if (inheritsFrom != null) + json.addProperty("inheritsFrom", inheritsFrom.toString()); + if (jar != null) + json.addProperty("jar", jar.toString()); + if (assetIndex != null) + json.add("assetIndex", JsonUtils.GSON.toJsonTree(assetIndex)); + if (assets != null) + json.addProperty("assets", assets); + if (complianceLevel != null) + json.addProperty("complianceLevel", complianceLevel); + if (javaVersion != null) + json.add("javaVersion", JsonUtils.GSON.toJsonTree(javaVersion)); + if (libraries != null) + json.add("libraries", JsonUtils.GSON.toJsonTree(libraries)); + if (compatibilityRules != null) + json.add("compatibilityRules", JsonUtils.GSON.toJsonTree(compatibilityRules)); + if (downloads != null) { + JsonObject downloadsObject = new JsonObject(); + for (Map.Entry entry : downloads.entrySet()) { + downloadsObject.add(entry.getKey().name().toLowerCase(Locale.ROOT), JsonUtils.GSON.toJsonTree(entry.getValue())); + } + json.add("downloads", downloadsObject); + } + if (logging != null) { + JsonObject loggingObject = new JsonObject(); + for (Map.Entry entry : logging.entrySet()) { + loggingObject.add(entry.getKey().name().toLowerCase(Locale.ROOT), JsonUtils.GSON.toJsonTree(entry.getValue())); + } + json.add("logging", loggingObject); + } + if (type != null) + json.addProperty("type", type.name().toLowerCase(Locale.ROOT)); + if (time != null) + json.addProperty("time", InstantTypeAdapter.serializeToString(time, ZoneOffset.UTC)); + if (releaseTime != null) + json.addProperty("releaseTime", InstantTypeAdapter.serializeToString(releaseTime, ZoneOffset.UTC)); + if (minimumLauncherVersion != null) + json.addProperty("minimumLauncherVersion", minimumLauncherVersion); + if (root != null) + json.addProperty("root", root); + if (hidden != null) + json.addProperty("hidden", hidden); + if (patches != null) { + JsonArray patchesArray = new JsonArray(patches.size()); + for (GameInstancePatch patch : patches) { + patchesArray.add(patch.toJsonObject()); + } + json.add("patches", patchesArray); + } + + return json; + } + + private static final class Builder { + // @formatter:off + private @Nullable GameInstanceID id; + private @Nullable String minecraftArguments; + private @Nullable Arguments arguments; + private @Nullable String mainClass; + private @Nullable GameInstanceID inheritsFrom; + private @Nullable GameInstanceID jar; + private @Nullable AssetIndexInfo assetIndex; + private @Nullable String assets; + private @Nullable Integer complianceLevel; + private @Nullable GameJavaVersion javaVersion; + private @Nullable @Unmodifiable List libraries; + private @Nullable @Unmodifiable List compatibilityRules; + private @Nullable @Unmodifiable Map downloads; + private @Nullable @Unmodifiable Map logging; + private @Nullable ReleaseType type; + private @Nullable Instant time; + private @Nullable Instant releaseTime; + private @Nullable Integer minimumLauncherVersion; + private @Nullable Boolean root; + private @Nullable Boolean hidden; + private @Nullable @Unmodifiable List patches; + private @Nullable JsonObject rawJson; + // @formatter:on + + Builder() { + } + + Builder(GameInstanceManifest manifest) { + this.id = manifest.id; + this.minecraftArguments = manifest.minecraftArguments; + this.arguments = manifest.arguments; + this.mainClass = manifest.mainClass; + this.inheritsFrom = manifest.inheritsFrom; + this.jar = manifest.jar; + this.assetIndex = manifest.assetIndex; + this.assets = manifest.assets; + this.complianceLevel = manifest.complianceLevel; + this.javaVersion = manifest.javaVersion; + this.libraries = manifest.libraries; + this.compatibilityRules = manifest.compatibilityRules; + this.downloads = manifest.downloads; + this.logging = manifest.logging; + this.type = manifest.type; + this.time = manifest.time; + this.releaseTime = manifest.releaseTime; + this.minimumLauncherVersion = manifest.minimumLauncherVersion; + this.root = manifest.root; + this.hidden = manifest.hidden; + this.patches = manifest.patches; + this.rawJson = manifest.rawJson != null ? manifest.rawJson.deepCopy() : null; + } + + public void setId(GameInstanceID id) { + this.id = id; + if (rawJson != null) { + rawJson.addProperty("id", id.toString()); + } + } + + public void setJar(@Nullable GameInstanceID jar) { + this.jar = jar; + if (rawJson != null) { + if (jar != null) { + rawJson.addProperty("jar", jar.toString()); + } else { + rawJson.remove("jar"); + } + } + } + + public void setMinecraftArguments(@Nullable String minecraftArguments) { + this.minecraftArguments = minecraftArguments; + if (rawJson != null) { + if (minecraftArguments != null) { + rawJson.addProperty("minecraftArguments", minecraftArguments); + } else { + rawJson.remove("minecraftArguments"); + } + } + } + + public void setArguments(@Nullable Arguments arguments) { + this.arguments = arguments; + if (rawJson != null) { + if (arguments != null) { + rawJson.add("arguments", JsonUtils.GSON.toJsonTree(arguments)); + } else { + rawJson.remove("arguments"); + } + } + } + + public void setMainClass(@Nullable String mainClass) { + this.mainClass = mainClass; + if (rawJson != null) { + if (mainClass != null) { + rawJson.addProperty("mainClass", mainClass); + } else { + rawJson.remove("mainClass"); + } + } + } + + public void setInheritsFrom(@Nullable GameInstanceID inheritsFrom) { + this.inheritsFrom = inheritsFrom; + if (rawJson != null) { + if (inheritsFrom != null) { + rawJson.addProperty("inheritsFrom", inheritsFrom.toString()); + } else { + rawJson.remove("inheritsFrom"); + } + } + } + + public void setAssetIndex(@Nullable AssetIndexInfo assetIndex) { + this.assetIndex = assetIndex; + if (rawJson != null) { + if (assetIndex != null) { + rawJson.add("assetIndex", JsonUtils.GSON.toJsonTree(assetIndex)); + } else { + rawJson.remove("assetIndex"); + } + } + } + + public void setJavaVersion(@Nullable GameJavaVersion javaVersion) { + this.javaVersion = javaVersion; + if (rawJson != null) { + if (javaVersion != null) { + rawJson.add("javaVersion", JsonUtils.GSON.toJsonTree(javaVersion)); + } else { + rawJson.remove("javaVersion"); + } + } + } + + public void setLibraries(@Nullable List libraries) { + this.libraries = libraries == null ? null : List.copyOf(libraries); + if (rawJson != null) { + if (this.libraries != null) { + rawJson.add("libraries", JsonUtils.GSON.toJsonTree(this.libraries)); + } else { + rawJson.remove("libraries"); + } + } + } + + public void setDownloads(@Nullable Map downloads) { + this.downloads = downloads == null ? null : ImmutableSequencedMap.copyOf(downloads); + if (rawJson != null) { + if (this.downloads != null) { + JsonObject downloadsObject = new JsonObject(); + for (Map.Entry entry : this.downloads.entrySet()) { + downloadsObject.add(entry.getKey().name(), JsonUtils.GSON.toJsonTree(entry.getValue())); + } + rawJson.add("downloads", downloadsObject); + } else { + rawJson.remove("downloads"); + } + } + } + + public void setLogging(@Nullable Map logging) { + this.logging = logging == null ? null : ImmutableSequencedMap.copyOf(logging); + if (rawJson != null) { + if (this.logging != null) { + JsonObject loggingObject = new JsonObject(); + for (Map.Entry entry : this.logging.entrySet()) { + loggingObject.add(entry.getKey().name(), JsonUtils.GSON.toJsonTree(entry.getValue())); + } + rawJson.add("logging", loggingObject); + } else { + rawJson.remove("logging"); + } + } + } + + public void setRoot(@Nullable Boolean root) { + this.root = root; + if (rawJson != null) { + if (root != null) { + rawJson.addProperty("root", root); + } else { + rawJson.remove("root"); + } + } + } + + public void setPatches(@Nullable List patches) { + if (patches != null) { + this.patches = List.copyOf(patches); + if (rawJson != null) { + JsonArray array = new JsonArray(this.patches.size()); + for (GameInstancePatch patch : this.patches) { + array.add(patch.toJsonObject()); + } + rawJson.add("patches", array); + } + } else { + this.patches = null; + if (rawJson != null) { + rawJson.remove("patches"); + } + } + } + + GameInstanceManifest toManifest() { + if (id == null) { + throw new IllegalStateException("id is null"); + } + + return new GameInstanceManifest( + id, + minecraftArguments, + arguments, + mainClass, + inheritsFrom, + jar, + assetIndex, + assets, + complianceLevel, + javaVersion, + libraries, + compatibilityRules, + downloads, + logging, + type, + time, + releaseTime, + minimumLauncherVersion, + root, + hidden, + patches, + rawJson + ); + } + } + + static final class Adapter extends TypeAdapter<@Nullable GameInstanceManifest> { + + @Override + public @Nullable GameInstanceManifest read(JsonReader in) { + JsonElement jsonElement = JsonParser.parseReader(in); + if (jsonElement.isJsonNull()) { + return null; + } + + if (jsonElement instanceof JsonObject jsonObject) { + return GameInstanceManifest.fromJson(jsonObject, false); + } + + throw new JsonParseException("Expected JsonObject but got " + jsonElement.getClass().getName()); + } + + @Override + public void write(JsonWriter out, @Nullable GameInstanceManifest value) throws IOException { + if (value != null) + JsonUtils.GSON.toJson(value.toJsonObject(), out); + else + out.nullValue(); + } + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java new file mode 100644 index 00000000000..ab2d72a07f5 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameInstancePatch.java @@ -0,0 +1,893 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.game; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import org.jackhuang.hmcl.util.ImmutableSequencedMap; +import org.jackhuang.hmcl.util.Lang; +import org.jackhuang.hmcl.util.gson.InstantTypeAdapter; +import org.jackhuang.hmcl.util.gson.JsonUtils; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.time.Instant; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +@NotNullByDefault +public record GameInstancePatch( + @Nullable String id, + @Nullable String version, + @Nullable Integer priority, + @Nullable String minecraftArguments, + @Nullable Arguments arguments, + @Nullable String mainClass, + @Nullable GameInstanceID inheritsFrom, + @Nullable GameInstanceID jar, + @Nullable AssetIndexInfo assetIndex, + @Nullable String assets, + @Nullable Integer complianceLevel, + @Nullable GameJavaVersion javaVersion, + @Nullable @Unmodifiable List libraries, + @Nullable @Unmodifiable List compatibilityRules, + @Nullable @Unmodifiable Map downloads, + @Nullable @Unmodifiable Map logging, + @Nullable ReleaseType type, + @Nullable Instant time, + @Nullable Instant releaseTime, + @Nullable Integer minimumLauncherVersion, + @Nullable Boolean hidden, + @Nullable @Unmodifiable JsonObject rawJson +) { + + /// Priority for the Minecraft base patch. + public static final int PRIORITY_MC = 0; + + /// Priority for loader patches. + public static final int PRIORITY_LOADER = 30000; + + /// Creates an empty patch with the given id. + public GameInstancePatch(String id) { + this( + id, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null); + } + + /// Creates a patch with launch metadata. + public GameInstancePatch( + String id, + @Nullable String version, + int priority, + @Nullable Arguments arguments, + @Nullable String mainClass, + @Nullable List libraries) { + this( + id, + version, + priority, + null, + arguments, + mainClass, + null, + null, + null, + null, + null, + null, + libraries, + null, + null, + null, + null, + null, + null, + null, + null, + null); + } + + /// Creates a patch from manifest metadata. + public static GameInstancePatch fromManifest( + GameInstanceManifest manifest, + String id, + @Nullable String version, + int priority) { + return new GameInstancePatch( + id, + version, + priority, + manifest.minecraftArguments(), + manifest.arguments(), + manifest.mainClass(), + manifest.inheritsFrom(), + manifest.jar(), + manifest.assetIndex(), + manifest.assets(), + manifest.complianceLevel(), + manifest.javaVersion(), + manifest.libraries(), + manifest.compatibilityRules(), + manifest.downloads(), + manifest.logging(), + manifest.type(), + manifest.time(), + manifest.releaseTime(), + manifest.minimumLauncherVersion(), + manifest.hidden(), + null); + } + + /// Returns the patch priority. + public int getPriority() { + return priority == null ? Integer.MIN_VALUE : priority; + } + + /// Returns libraries. + public List getLibraries() { + return libraries == null ? List.of() : libraries; + } + + /// Returns whether the patch is hidden. + public boolean isHidden() { + return hidden != null && hidden; + } + + /// Returns a patch copy with the given id. + public GameInstancePatch withId(@Nullable String id) { + if (Objects.equals(this.id, id)) { + return this; + } + + Builder builder = new Builder(this); + builder.setId(id); + return builder.toPatch(); + } + + /// Returns a patch copy with the given version. + public GameInstancePatch withVersion(@Nullable String version) { + if (Objects.equals(this.version, version)) { + return this; + } + + Builder builder = new Builder(this); + builder.setVersion(version); + return builder.toPatch(); + } + + /// Returns a patch copy with the given priority. + public GameInstancePatch withPriority(@Nullable Integer priority) { + if (Objects.equals(this.priority, priority)) { + return this; + } + + Builder builder = new Builder(this); + builder.setPriority(priority); + return builder.toPatch(); + } + + /// Returns a patch copy with the given jar id. + public GameInstancePatch withJar(@Nullable GameInstanceID jar) { + if (Objects.equals(this.jar, jar)) { + return this; + } + + Builder builder = new Builder(this); + builder.setJar(jar); + return builder.toPatch(); + } + + /// Returns a patch copy with the given libraries. + public GameInstancePatch withLibraries(@Nullable List libraries) { + if (this.libraries == libraries) { + return this; + } + + Builder builder = new Builder(this); + builder.setLibraries(libraries); + return builder.toPatch(); + } + + /// Returns a patch copy with the given Java version. + public GameInstancePatch withJavaVersion(@Nullable GameJavaVersion javaVersion) { + if (this.javaVersion == javaVersion) { + return this; + } + + Builder builder = new Builder(this); + builder.setJavaVersion(javaVersion); + return builder.toPatch(); + } + + /// Returns a patch copy with the given arguments. + public GameInstancePatch withArguments(@Nullable Arguments arguments) { + if (this.arguments == arguments) { + return this; + } + + Builder builder = new Builder(this); + builder.setArguments(arguments); + return builder.toPatch(); + } + + /// Returns a patch copy with the given main class. + public GameInstancePatch withMainClass(@Nullable String mainClass) { + if (Objects.equals(this.mainClass, mainClass)) { + return this; + } + + Builder builder = new Builder(this); + builder.setMainClass(mainClass); + return builder.toPatch(); + } + + /// Returns a patch copy with the given asset index. + public GameInstancePatch withAssetIndex(@Nullable AssetIndexInfo assetIndex) { + if (this.assetIndex == assetIndex) { + return this; + } + + Builder builder = new Builder(this); + builder.setAssetIndex(assetIndex); + return builder.toPatch(); + } + + /// Returns a patch copy with the given downloads. + public GameInstancePatch withDownload(@Nullable Map downloads) { + if (this.downloads == downloads) { + return this; + } + + Builder builder = new Builder(this); + builder.setDownloads(downloads); + return builder.toPatch(); + } + + /// Returns a patch copy with the given logging metadata. + public GameInstancePatch withLogging(@Nullable Map logging) { + if (this.logging == logging) { + return this; + } + + Builder builder = new Builder(this); + builder.setLogging(logging); + return builder.toPatch(); + } + + private static final class Builder { + private @Nullable String id; + private @Nullable String version; + private @Nullable Integer priority; + private @Nullable String minecraftArguments; + private @Nullable Arguments arguments; + private @Nullable String mainClass; + private @Nullable GameInstanceID inheritsFrom; + private @Nullable GameInstanceID jar; + private @Nullable AssetIndexInfo assetIndex; + private @Nullable String assets; + private @Nullable Integer complianceLevel; + private @Nullable GameJavaVersion javaVersion; + private @Nullable @Unmodifiable List libraries; + private @Nullable @Unmodifiable List compatibilityRules; + private @Nullable @Unmodifiable Map downloads; + private @Nullable @Unmodifiable Map logging; + private @Nullable ReleaseType type; + private @Nullable Instant time; + private @Nullable Instant releaseTime; + private @Nullable Integer minimumLauncherVersion; + private @Nullable Boolean hidden; + private @Nullable JsonObject rawJson; + + private Builder(GameInstancePatch patch) { + this.id = patch.id; + this.version = patch.version; + this.priority = patch.priority; + this.minecraftArguments = patch.minecraftArguments; + this.arguments = patch.arguments; + this.mainClass = patch.mainClass; + this.inheritsFrom = patch.inheritsFrom; + this.jar = patch.jar; + this.assetIndex = patch.assetIndex; + this.assets = patch.assets; + this.complianceLevel = patch.complianceLevel; + this.javaVersion = patch.javaVersion; + this.libraries = patch.libraries; + this.compatibilityRules = patch.compatibilityRules; + this.downloads = patch.downloads; + this.logging = patch.logging; + this.type = patch.type; + this.time = patch.time; + this.releaseTime = patch.releaseTime; + this.minimumLauncherVersion = patch.minimumLauncherVersion; + this.hidden = patch.hidden; + this.rawJson = patch.rawJson != null ? patch.rawJson.deepCopy() : null; + } + + private void setId(@Nullable String id) { + this.id = id; + if (rawJson != null) { + if (id != null) { + rawJson.addProperty("id", id); + } else { + rawJson.remove("id"); + } + } + } + + private void setVersion(@Nullable String version) { + this.version = version; + if (rawJson != null) { + if (version != null) { + rawJson.addProperty("version", version); + } else { + rawJson.remove("version"); + } + } + } + + private void setPriority(@Nullable Integer priority) { + this.priority = priority; + if (rawJson != null) { + if (priority != null) { + rawJson.addProperty("priority", priority); + } else { + rawJson.remove("priority"); + } + } + } + + private void setMinecraftArguments(@Nullable String minecraftArguments) { + this.minecraftArguments = minecraftArguments; + if (rawJson != null) { + if (minecraftArguments != null) { + rawJson.addProperty("minecraftArguments", minecraftArguments); + } else { + rawJson.remove("minecraftArguments"); + } + } + } + + private void setArguments(@Nullable Arguments arguments) { + this.arguments = arguments; + if (rawJson != null) { + if (arguments != null) { + rawJson.add("arguments", JsonUtils.GSON.toJsonTree(arguments)); + } else { + rawJson.remove("arguments"); + } + } + } + + private void setMainClass(@Nullable String mainClass) { + this.mainClass = mainClass; + if (rawJson != null) { + if (mainClass != null) { + rawJson.addProperty("mainClass", mainClass); + } else { + rawJson.remove("mainClass"); + } + } + } + + private void setInheritsFrom(@Nullable GameInstanceID inheritsFrom) { + this.inheritsFrom = inheritsFrom; + if (rawJson != null) { + if (inheritsFrom != null) { + rawJson.addProperty("inheritsFrom", inheritsFrom.toString()); + } else { + rawJson.remove("inheritsFrom"); + } + } + } + + private void setJar(@Nullable GameInstanceID jar) { + this.jar = jar; + if (rawJson != null) { + if (jar != null) { + rawJson.addProperty("jar", jar.toString()); + } else { + rawJson.remove("jar"); + } + } + } + + private void setAssetIndex(@Nullable AssetIndexInfo assetIndex) { + this.assetIndex = assetIndex; + if (rawJson != null) { + if (assetIndex != null) { + rawJson.add("assetIndex", JsonUtils.GSON.toJsonTree(assetIndex)); + } else { + rawJson.remove("assetIndex"); + } + } + } + + private void setAssets(@Nullable String assets) { + this.assets = assets; + if (rawJson != null) { + if (assets != null) { + rawJson.addProperty("assets", assets); + } else { + rawJson.remove("assets"); + } + } + } + + private void setComplianceLevel(@Nullable Integer complianceLevel) { + this.complianceLevel = complianceLevel; + if (rawJson != null) { + if (complianceLevel != null) { + rawJson.addProperty("complianceLevel", complianceLevel); + } else { + rawJson.remove("complianceLevel"); + } + } + } + + private void setJavaVersion(@Nullable GameJavaVersion javaVersion) { + this.javaVersion = javaVersion; + if (rawJson != null) { + if (javaVersion != null) { + rawJson.add("javaVersion", JsonUtils.GSON.toJsonTree(javaVersion)); + } else { + rawJson.remove("javaVersion"); + } + } + } + + private void setLibraries(@Nullable List libraries) { + this.libraries = libraries == null ? null : List.copyOf(libraries); + if (rawJson != null) { + if (this.libraries != null) { + rawJson.add("libraries", JsonUtils.GSON.toJsonTree(this.libraries)); + } else { + rawJson.remove("libraries"); + } + } + } + + private void setCompatibilityRules(@Nullable List compatibilityRules) { + this.compatibilityRules = compatibilityRules == null ? null : List.copyOf(compatibilityRules); + if (rawJson != null) { + if (this.compatibilityRules != null) { + rawJson.add("compatibilityRules", JsonUtils.GSON.toJsonTree(this.compatibilityRules)); + } else { + rawJson.remove("compatibilityRules"); + } + } + } + + private void setDownloads(@Nullable Map downloads) { + this.downloads = downloads == null ? null : ImmutableSequencedMap.copyOf(downloads); + if (rawJson != null) { + if (this.downloads != null) { + JsonObject downloadsObject = new JsonObject(); + for (Map.Entry entry : this.downloads.entrySet()) { + downloadsObject.add(entry.getKey().name(), JsonUtils.GSON.toJsonTree(entry.getValue())); + } + rawJson.add("downloads", downloadsObject); + } else { + rawJson.remove("downloads"); + } + } + } + + private void setLogging(@Nullable Map logging) { + this.logging = logging == null ? null : ImmutableSequencedMap.copyOf(logging); + if (rawJson != null) { + if (this.logging != null) { + JsonObject loggingObject = new JsonObject(); + for (Map.Entry entry : this.logging.entrySet()) { + loggingObject.add(entry.getKey().name(), JsonUtils.GSON.toJsonTree(entry.getValue())); + } + rawJson.add("logging", loggingObject); + } else { + rawJson.remove("logging"); + } + } + } + + private void setType(@Nullable ReleaseType type) { + this.type = type; + if (rawJson != null) { + if (type != null) { + rawJson.addProperty("type", type.name()); + } else { + rawJson.remove("type"); + } + } + } + + private void setTime(@Nullable Instant time) { + this.time = time; + if (rawJson != null) { + if (time != null) { + rawJson.addProperty("time", InstantTypeAdapter.serializeToString(time, ZoneOffset.UTC)); + } else { + rawJson.remove("time"); + } + } + } + + private void setReleaseTime(@Nullable Instant releaseTime) { + this.releaseTime = releaseTime; + if (rawJson != null) { + if (releaseTime != null) { + rawJson.addProperty("releaseTime", InstantTypeAdapter.serializeToString(releaseTime, ZoneOffset.UTC)); + } else { + rawJson.remove("releaseTime"); + } + } + } + + private void setMinimumLauncherVersion(@Nullable Integer minimumLauncherVersion) { + this.minimumLauncherVersion = minimumLauncherVersion; + if (rawJson != null) { + if (minimumLauncherVersion != null) { + rawJson.addProperty("minimumLauncherVersion", minimumLauncherVersion); + } else { + rawJson.remove("minimumLauncherVersion"); + } + } + } + + private void setHidden(@Nullable Boolean hidden) { + this.hidden = hidden; + if (rawJson != null) { + if (hidden != null) { + rawJson.addProperty("hidden", hidden); + } else { + rawJson.remove("hidden"); + } + } + } + + private GameInstancePatch toPatch() { + return new GameInstancePatch( + id, + version, + priority, + minecraftArguments, + arguments, + mainClass, + inheritsFrom, + jar, + assetIndex, + assets, + complianceLevel, + javaVersion, + libraries, + compatibilityRules, + downloads, + logging, + type, + time, + releaseTime, + minimumLauncherVersion, + hidden, + rawJson); + } + } + + public static GameInstancePatch fromJson(JsonObject json) throws JsonParseException { + @Nullable String id = null; + @Nullable String version = null; + @Nullable Integer priority = null; + @Nullable String minecraftArguments = null; + @Nullable Arguments arguments = null; + @Nullable String mainClass = null; + @Nullable GameInstanceID inheritsFrom = null; + @Nullable GameInstanceID jar = null; + @Nullable AssetIndexInfo assetIndex = null; + @Nullable String assets = null; + @Nullable Integer complianceLevel = null; + @Nullable GameJavaVersion javaVersion = null; + @Nullable List libraries = null; + @Nullable List compatibilityRules = null; + @Nullable Map downloads = null; + @Nullable Map logging = null; + @Nullable ReleaseType type = null; + @Nullable Instant time = null; + @Nullable Instant releaseTime = null; + @Nullable Integer minimumLauncherVersion = null; + @Nullable Boolean hidden = null; + + for (Map.Entry entry : json.entrySet()) { + String memberName = entry.getKey(); + JsonElement value = entry.getValue(); + + switch (memberName) { + case "id" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + id = primitive.getAsString(); + } + } + case "version" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + version = primitive.getAsString(); + } + } + case "priority" -> { + if (value instanceof JsonPrimitive primitive && primitive.isNumber()) { + priority = primitive.getAsInt(); + } + } + case "minecraftArguments" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + minecraftArguments = primitive.getAsString(); + } + } + case "arguments" -> arguments = JsonUtils.GSON.fromJson(value, Arguments.class); + case "mainClass" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + mainClass = primitive.getAsString(); + } + } + case "inheritsFrom" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + inheritsFrom = new GameInstanceID(primitive.getAsString()); + } catch (IllegalArgumentException ignored) { + } + } + } + case "jar" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + jar = new GameInstanceID(primitive.getAsString()); + } catch (IllegalArgumentException ignored) { + } + } + } + case "assetIndex" -> assetIndex = JsonUtils.GSON.fromJson(value, AssetIndexInfo.class); + case "assets" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + assets = primitive.getAsString(); + } + } + case "complianceLevel" -> { + if (value instanceof JsonPrimitive primitive && primitive.isNumber()) { + complianceLevel = primitive.getAsInt(); + } + } + case "javaVersion" -> javaVersion = JsonUtils.GSON.fromJson(value, GameJavaVersion.class); + case "libraries" -> { + if (value instanceof JsonArray array) { + List list = new ArrayList<>(array.size()); + for (JsonElement element : array) { + if (element instanceof JsonObject object) { + list.add(Library.fromJson(object)); + } + } + libraries = List.copyOf(list); + } + } + case "compatibilityRules" -> { + if (value instanceof JsonArray array) { + List list = new ArrayList<>(array.size()); + for (JsonElement element : array) { + list.add(JsonUtils.GSON.fromJson(element, CompatibilityRule.class)); + } + compatibilityRules = List.copyOf(list); + } + } + case "downloads" -> { + if (value instanceof JsonObject object) { + Map map = new LinkedHashMap<>(); + for (Map.Entry downloadEntry : object.entrySet()) { + try { + DownloadType downloadType = DownloadType.valueOf(downloadEntry.getKey()); + map.put(downloadType, JsonUtils.GSON.fromJson(downloadEntry.getValue(), DownloadInfo.class)); + } catch (IllegalArgumentException ignored) { + } + } + downloads = ImmutableSequencedMap.copyOf(map); + } + } + case "logging" -> { + if (value instanceof JsonObject object) { + Map map = new LinkedHashMap<>(); + for (Map.Entry loggingEntry : object.entrySet()) { + try { + DownloadType downloadType = DownloadType.valueOf(loggingEntry.getKey()); + map.put(downloadType, JsonUtils.GSON.fromJson(loggingEntry.getValue(), LoggingInfo.class)); + } catch (IllegalArgumentException ignored) { + } + } + logging = ImmutableSequencedMap.copyOf(map); + } + } + case "type" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + type = ReleaseType.valueOf(primitive.getAsString()); + } catch (IllegalArgumentException ignored) { + } + } + } + case "time" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + time = InstantTypeAdapter.deserializeToInstant(primitive.getAsString()); + } catch (Exception ignored) { + } + } + } + case "releaseTime" -> { + if (value instanceof JsonPrimitive primitive && primitive.isString()) { + try { + releaseTime = InstantTypeAdapter.deserializeToInstant(primitive.getAsString()); + } catch (Exception ignored) { + } + } + } + case "minimumLauncherVersion" -> { + if (value instanceof JsonPrimitive primitive && primitive.isNumber()) { + minimumLauncherVersion = primitive.getAsInt(); + } + } + case "hidden" -> { + if (value instanceof JsonPrimitive primitive && primitive.isBoolean()) { + hidden = primitive.getAsBoolean(); + } + } + } + } + + return new GameInstancePatch( + id, + version, + priority, + minecraftArguments, + arguments, + mainClass, + inheritsFrom, + jar, + assetIndex, + assets, + complianceLevel, + javaVersion, + libraries, + compatibilityRules, + downloads, + logging, + type, + time, + releaseTime, + minimumLauncherVersion, + hidden, + json.deepCopy()); + } + + public JsonObject toJsonObject() { + if (rawJson != null) { + return rawJson.deepCopy(); + } + + JsonObject json = new JsonObject(); + + if (id != null) + json.addProperty("id", id); + if (version != null) + json.addProperty("version", version); + if (priority != null) + json.addProperty("priority", priority); + if (minecraftArguments != null) + json.addProperty("minecraftArguments", minecraftArguments); + if (arguments != null) + json.add("arguments", JsonUtils.GSON.toJsonTree(arguments)); + if (mainClass != null) + json.addProperty("mainClass", mainClass); + if (inheritsFrom != null) + json.addProperty("inheritsFrom", inheritsFrom.toString()); + if (jar != null) + json.addProperty("jar", jar.toString()); + if (assetIndex != null) + json.add("assetIndex", JsonUtils.GSON.toJsonTree(assetIndex)); + if (assets != null) + json.addProperty("assets", assets); + if (complianceLevel != null) + json.addProperty("complianceLevel", complianceLevel); + if (javaVersion != null) + json.add("javaVersion", JsonUtils.GSON.toJsonTree(javaVersion)); + if (libraries != null) + json.add("libraries", JsonUtils.GSON.toJsonTree(libraries)); + if (compatibilityRules != null) + json.add("compatibilityRules", JsonUtils.GSON.toJsonTree(compatibilityRules)); + if (downloads != null) { + JsonObject downloadsObject = new JsonObject(); + for (Map.Entry entry : downloads.entrySet()) { + downloadsObject.add(entry.getKey().name(), JsonUtils.GSON.toJsonTree(entry.getValue())); + } + json.add("downloads", downloadsObject); + } + if (logging != null) { + JsonObject loggingObject = new JsonObject(); + for (Map.Entry entry : logging.entrySet()) { + loggingObject.add(entry.getKey().name(), JsonUtils.GSON.toJsonTree(entry.getValue())); + } + json.add("logging", loggingObject); + } + if (type != null) + json.addProperty("type", type.name()); + if (time != null) + json.addProperty("time", InstantTypeAdapter.serializeToString(time, ZoneOffset.UTC)); + if (releaseTime != null) + json.addProperty("releaseTime", InstantTypeAdapter.serializeToString(releaseTime, ZoneOffset.UTC)); + if (minimumLauncherVersion != null) + json.addProperty("minimumLauncherVersion", minimumLauncherVersion); + if (hidden != null) + json.addProperty("hidden", hidden); + + return json; + } + + GameInstanceManifest merge(GameInstanceManifest parent) { + return new GameInstanceManifest( + parent.id(), + minecraftArguments == null ? parent.minecraftArguments() : minecraftArguments, + Arguments.merge(parent.arguments(), arguments), + mainClass == null ? parent.mainClass() : mainClass, + null, // inheritsFrom + parent.jar(), + assetIndex == null ? parent.assetIndex() : assetIndex, + assets == null ? parent.assets() : assets, + complianceLevel, + javaVersion == null ? parent.javaVersion() : javaVersion, + Lang.merge(this.libraries, parent.libraries()), + Lang.merge(parent.compatibilityRules(), this.compatibilityRules), + downloads == null ? parent.downloads() : downloads, + logging == null ? parent.logging() : logging, + type == null ? parent.type() : type, + time == null ? parent.time() : time, + releaseTime == null ? parent.releaseTime() : releaseTime, + Lang.merge(minimumLauncherVersion, parent.minimumLauncherVersion(), Math::max), + true, + hidden, + parent.patches(), + null); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java index 1b7cc27dcd6..5949fac8d57 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/GameRepository.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui 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 @@ -20,6 +20,7 @@ import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.Platform; +import org.jetbrains.annotations.NotNullByDefault; import java.io.IOException; import java.nio.file.Files; @@ -29,248 +30,208 @@ import java.util.Optional; import java.util.Set; -/** - * Supports operations on versioning. - *

- * Note that game repository will not do any operations which need connection with Internet, if do, - * see {@link org.jackhuang.hmcl.download.DependencyManager} - * - * @author huangyuhui - */ -public interface GameRepository extends VersionProvider { - - /** - * Does the version of id exist? - * - * @param id the id of version - * @return true if the version exists - */ - @Override - boolean hasVersion(String id); - - /** - * Get the version - * - * @param id the id of version - * @return the version you want - * @throws VersionNotFoundException if no version is id. - */ - @Override - Version getVersion(String id) throws VersionNotFoundException; - - default Version getResolvedVersion(String id) throws VersionNotFoundException { - return getVersion(id).resolve(this); - } +/// Provides indexed access to local game instances and the filesystem layout used by those instances. +/// +/// Implementations are responsible for loading instance manifests, resolving inheritance and patches, +/// locating instance-owned files, and exposing helper paths used by launch, download, and maintenance code. +@NotNullByDefault +public interface GameRepository { + /// Resolves inheritance into launch and standalone manifest views. + /// + /// @param manifest the manifest to resolve + /// @return the resolved manifest view + GameInstanceManifest.Resolved resolve(GameInstanceManifest manifest) throws NoSuchGameInstanceException; - default Version getResolvedPreservingPatchesVersion(String id) throws VersionNotFoundException { - return getVersion(id).resolvePreservingPatches(this); - } + /// Returns whether the instance exists in the current repository index. + /// + /// @param instanceId the instance id + /// @return whether the instance exists + boolean hasInstance(GameInstanceID instanceId); + + /// Returns the stored manifest for an instance without resolving inheritance or patches. + /// + /// @param instanceId the instance id + /// @return the stored instance manifest + /// @throws NoSuchGameInstanceException if the instance is not loaded in this repository + GameInstanceManifest getInstanceManifest(GameInstanceID instanceId) throws NoSuchGameInstanceException; + + /// Returns a cached launch-ready manifest view for the instance. + /// + /// @param instanceId the instance id + /// @return the resolved manifest view + GameInstanceManifest.Resolved getResolvedInstanceManifest(GameInstanceID instanceId) throws NoSuchGameInstanceException; + + /// Returns the number of loaded instances. + /// + /// @return the loaded instance count + int getInstanceCount(); + + /// Returns the stored manifests for all loaded instances. + /// + /// @return the loaded instance manifests + Collection getInstanceManifests(); + + /// Reloads repository state from the backing storage. + void refresh(); - /** - * How many version are there? - */ - int getVersionCount(); - - /** - * Gets the collection of versions - * - * @return the collection of versions - */ - Collection getVersions(); - - /** - * Load version list. - *

- * This method should be called before launching a version. - * A time-costly operation. - * You'd better execute this method in a new thread. - */ - void refreshVersions(); - - default Task refreshVersionsAsync() { - return Task.runAsync(this::refreshVersions); + /// Creates a task that reloads repository state from the backing storage. + /// + /// @return a task that calls [#refresh()] + default Task refreshAsync() { + return Task.runAsync(this::refresh); } - /** - * Gets the root folder of specific version. - * The root folders the versions must be unique. - * For example, .minecraft/versions/<version name>/. - */ - Path getVersionRoot(String id); - - /** - * Gets the current running directory of the given version for game. - * - * @param id the version id - */ - Path getRunDirectory(String id); - - Path getLibrariesDirectory(Version version); - - /** - * Get the library file in disk. - * This method allows versions and libraries that are not loaded by this game repository. - * - * @param version the reference of game version - * @param lib the library, {@link Version#getLibraries()} - * @return the library file - */ - Path getLibraryFile(Version version, Library lib); - - /** - * Get the directory that native libraries will be unzipped to. - *

- * You'd better return a unique directory. - * Or if it returns a temporary directory, {@link org.jackhuang.hmcl.launch.Launcher#makeLaunchScript} will fail. - * If you do want to return a temporary directory, make {@link org.jackhuang.hmcl.launch.Launcher#makeLaunchScript} - * always fail({@code UnsupportedOperationException}) and not to use it. - * - * @param id version id - * @param platform the platform of native libraries - * @return the native directory - */ - Path getNativeDirectory(String id, Platform platform); - - /// Get the directory for placing mod files. + /// Returns the directory that stores files belonging to an instance. + /// + /// @param instanceId the instance id + /// @return the instance root directory + Path getInstanceRoot(GameInstanceID instanceId); + + /// Returns the working directory used when launching an instance. + /// + /// @param instanceId the instance id + /// @return the run directory + Path getRunDirectory(GameInstanceID instanceId); + + /// Returns the base directory used to store shared libraries for a manifest. + /// + /// @param manifest the manifest whose libraries are being resolved + /// @return the libraries directory + Path getLibrariesDirectory(GameInstanceManifest manifest); + + /// Returns the expected filesystem path for a library. + /// + /// @param manifest the manifest that owns or references the library + /// @param lib the library descriptor + /// @return the library file path + Path getLibraryFile(GameInstanceManifest manifest, Library lib); + + /// Returns the directory used for extracted native libraries of an instance and platform. + /// + /// @param instanceId the instance id + /// @param platform the target platform + /// @return the native library directory + Path getNativeDirectory(GameInstanceID instanceId, Platform platform); + + /// Returns the mods directory for an instance. /// - /// @param id instance id + /// @param instanceId the instance id /// @return the mods directory - Path getModsDirectory(String id); + Path getModsDirectory(GameInstanceID instanceId); - /// Get the directory for placing resource packs. + /// Returns the resource pack directory for an instance. /// - /// @param id instance id + /// @param instanceId the instance id /// @return the resource pack directory - Path getResourcePackDirectory(String id); - - /** - * Get minecraft jar - * - * @param version resolvedVersion - * @return the minecraft jar - */ - Path getVersionJar(Version version); - - /** - * Detect game version. - *

- * This method is time-consuming, but the result will be cached. - * Consider running this job in IO scheduler. - * - * @param version version - * @return game version, or empty if an error occurred in detection. - */ - Optional getGameVersion(Version version); - - /** - * Detect game version. - *

- * This method is time-consuming, but the result will be cached. - * Consider running this job in IO scheduler. - * - * @param versionId id of version - * @return game version, or empty if an error occurred in detection. - */ - default Optional getGameVersion(String versionId) throws VersionNotFoundException { - return getGameVersion(getVersion(versionId)); + Path getResourcePackDirectory(GameInstanceID instanceId); + + /// Returns the primary client jar path for a manifest. + /// + /// @param manifest the manifest whose jar should be located + /// @return the primary client jar path + Path getInstanceJar(GameInstanceManifest manifest); + + /// Detects the Minecraft game version associated with a manifest. + /// + /// @param manifest the manifest to inspect + /// @return the detected Minecraft game version, or empty if it cannot be determined + Optional getGameVersion(GameInstanceManifest manifest); + + /// Detects the Minecraft game version associated with an instance. + /// + /// @param instanceId the instance id + /// @return the detected Minecraft game version, or empty if it cannot be determined + /// @throws NoSuchGameInstanceException if the instance is not loaded in this repository + default Optional getGameVersion(GameInstanceID instanceId) throws NoSuchGameInstanceException { + return getGameVersion(getInstanceManifest(instanceId)); } - /** - * Get minecraft jar - * - * @param version version id - * @return the minecraft jar - */ - default Path getVersionJar(String version) throws VersionNotFoundException { - return getVersionJar(getVersion(version).resolve(this)); + /// Returns the primary client jar path for an instance. + /// + /// @param instanceId the instance id + /// @return the primary client jar path + /// @throws NoSuchGameInstanceException if the instance is not loaded in this repository + default Path getInstanceJar(GameInstanceID instanceId) throws NoSuchGameInstanceException { + return getInstanceJar(getResolvedInstanceManifest(instanceId).launchManifest()); } - /** - * Rename given version to new name. - * - * @param from The id of original version - * @param to The new id of the version - * @throws UnsupportedOperationException if this game repository does not support renaming a version - * @return true if the operation is done successfully, false if version `from` not found, version json is malformed or I/O errors occurred. - */ - boolean renameVersion(String from, String to); - - /** - * Get actual asset directory. - * Will reconstruct assets or do some blocking tasks if necessary. - * You'd better create a new thread to invoke this method. - * - * @param version the id of specific version that is relevant to {@code assetId} - * @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()} - * @return the actual asset directory - */ - Path getActualAssetDirectory(String version, String assetId); - - /** - * Get the asset directory according to the asset id. - * - * @param version the id of specific version that is relevant to {@code assetId} - * @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()} - * @return the asset directory - */ - Path getAssetDirectory(String version, String assetId); - - /** - * Get the file that given asset object refers to - * - * @param version the id of specific version that is relevant to {@code assetId} - * @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()} - * @param name the asset object name, you can find it in keys of {@link AssetIndex#getObjects()} - * @throws java.io.IOException if I/O operation fails. - * @return the file that given asset object refers to - */ - Optional getAssetObject(String version, String assetId, String name) throws IOException; - - /** - * Get the file that given asset object refers to - * - * @param version the id of specific version that is relevant to {@code assetId} - * @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()} - * @param obj the asset object, you can find it in {@link AssetIndex#getObjects()} - * @return the file that given asset object refers to - */ - Path getAssetObject(String version, String assetId, AssetObject obj); - - /** - * Get asset index that assetId represents - * - * @param version the id of specific version that is relevant to {@code assetId} - * @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()} - * @return the asset index - */ - AssetIndex getAssetIndex(String version, String assetId) throws IOException; - - /** - * Get the asset_index.json which includes asset objects information. - * - * @param version the id of specific version that is relevant to {@code assetId} - * @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()} - */ - Path getIndexFile(String version, String assetId); - - /** - * Get logging object - * - * @param version the id of specific version that is relevant to {@code assetId} - * @param assetId the asset id, you can find it in {@link AssetIndexInfo#getId()} {@link Version#getAssetIndex()} - * @param loggingInfo the logging info - * @return the file that loggingInfo refers to - */ - Path getLoggingObject(String version, String assetId, LoggingInfo loggingInfo); - - default Set getClasspath(Version version) { + /// Renames an instance and updates repository-managed references. + /// + /// @param from the current instance id + /// @param to the target instance id + /// @return whether the instance was renamed + boolean renameInstance(GameInstanceID from, GameInstanceID to); + + /// Returns the asset directory that should be used at launch time. + /// + /// @param instanceId the instance id + /// @param assetId the asset index id + /// @return the actual asset directory + Path getActualAssetDirectory(GameInstanceID instanceId, String assetId); + + /// Returns the base asset storage directory for an instance. + /// + /// @param instanceId the instance id + /// @param assetId the asset index id + /// @return the asset storage directory + Path getAssetDirectory(GameInstanceID instanceId, String assetId); + + /// Returns an existing asset object path by logical asset name. + /// + /// @param instanceId the instance id + /// @param assetId the asset index id + /// @param name the logical asset name + /// @return the asset object path, or empty if the object is not present in the asset index + /// @throws IOException if the asset index cannot be read + Optional getAssetObject(GameInstanceID instanceId, String assetId, String name) throws IOException; + + /// Returns the expected path for an asset object descriptor. + /// + /// @param instanceId the instance id + /// @param assetId the asset index id + /// @param obj the asset object descriptor + /// @return the asset object path + Path getAssetObject(GameInstanceID instanceId, String assetId, AssetObject obj); + + /// Reads an asset index. + /// + /// @param instanceId the instance id + /// @param assetId the asset index id + /// @return the asset index + /// @throws IOException if the asset index cannot be read + AssetIndex getAssetIndex(GameInstanceID instanceId, String assetId) throws IOException; + + /// Returns the path of an asset index file. + /// + /// @param instanceId the instance id + /// @param assetId the asset index id + /// @return the asset index file path + Path getIndexFile(GameInstanceID instanceId, String assetId); + + /// Returns the path of a logging configuration object. + /// + /// @param instanceId the instance id + /// @param assetId the asset index id used as the logging object namespace + /// @param loggingInfo the logging configuration descriptor + /// @return the logging object path + Path getLoggingObject(GameInstanceID instanceId, String assetId, LoggingInfo loggingInfo); + + /// Returns the classpath entries whose library files are present on disk. + /// + /// @param manifest the manifest whose libraries should be mapped to classpath entries + /// @return absolute classpath entries for existing non-native libraries + default Set getClasspath(GameInstanceManifest manifest) { Set classpath = new LinkedHashSet<>(); - for (Library library : version.getLibraries()) - if (library.appliesToCurrentEnvironment() && !library.isNative()) { - Path f = getLibraryFile(version, library); - if (Files.isRegularFile(f)) - classpath.add(FileUtils.getAbsolutePath(f)); - } + if (manifest.libraries() != null) { + for (Library library : manifest.libraries()) + if (library.appliesToCurrentEnvironment() && !library.isNative()) { + Path f = getLibraryFile(manifest, library); + if (Files.isRegularFile(f)) + classpath.add(FileUtils.getAbsolutePath(f)); + } + } + return classpath; } + } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java index f22f407d66b..6b2d0333f33 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/JavaVersionConstraint.java @@ -35,13 +35,13 @@ public enum JavaVersionConstraint { VANILLA(true, VersionRange.all(), VersionRange.all()) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { // Give priority to the Java version requirements specified in the version JSON - return version == null || version.getJavaVersion() == null; + return version == null || version.javaVersion() == null; } @Override - public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) { + public boolean checkJava(GameVersionNumber gameVersionNumber, GameInstanceManifest version, JavaRuntime java, LibraryAnalyzer analyzer) { GameJavaVersion minimumJavaVersion = GameJavaVersion.getMinimumJavaVersion(gameVersionNumber); return minimumJavaVersion == null || java.getParsedVersion() >= minimumJavaVersion.majorVersion(); } @@ -49,20 +49,20 @@ public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, J // Minecraft with suggested java version recorded in game json is restrictedly constrained. GAME_JSON(true, VersionRange.all(), VersionRange.all()) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { if (version == null) return false; // We only checks for 1.7.10 and above, since 1.7.2 with Forge can only run on Java 7, but it is recorded Java 8 in game json, which is not correct. - return gameVersionNumber.compareTo("1.7.10") >= 0 && version.getJavaVersion() != null; + return gameVersionNumber.compareTo("1.7.10") >= 0 && version.javaVersion() != null; } @Override - public VersionRange getJavaVersionRange(Version version, LibraryAnalyzer analyzer) { + public VersionRange getJavaVersionRange(GameInstanceManifest manifest, LibraryAnalyzer analyzer) { String javaVersion; - if (Objects.requireNonNull(version.getJavaVersion()).majorVersion() >= 9) { - javaVersion = "" + version.getJavaVersion().majorVersion(); + if (Objects.requireNonNull(manifest.javaVersion()).majorVersion() >= 9) { + javaVersion = "" + manifest.javaVersion().majorVersion(); } else { - javaVersion = "1." + version.getJavaVersion().majorVersion(); + javaVersion = "1." + manifest.javaVersion().majorVersion(); } return VersionNumber.atLeast(javaVersion); } @@ -70,7 +70,7 @@ public VersionRange getJavaVersionRange(Version version, LibraryA // Minecraft<=1.7.2+Forge requires Java<=7, But LegacyModFixer may fix that problem. So only suggest user using Java 7. MODDED_JAVA_7(false, GameVersionNumber.atMost("1.7.2"), VersionNumber.atMost("1.7.999")) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer); @@ -78,7 +78,7 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul }, MODDED_JAVA_8(false, GameVersionNumber.between("1.7.10", "1.16.999"), VersionNumber.between("1.8", "1.8.999")) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer); @@ -86,7 +86,7 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul }, MODDED_JAVA_16(false, GameVersionNumber.between("1.17", "1.17.999"), VersionNumber.between("16", "16.999")) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer); @@ -94,7 +94,7 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul }, MODDED_JAVA_17(false, GameVersionNumber.between("1.18", "1.20.4"), VersionNumber.between("17", "17.999")) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer); @@ -102,7 +102,7 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul }, MODDED_JAVA_21(false, GameVersionNumber.atLeast("1.20.5"), VersionNumber.between("21", "21.999")) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.FORGE) && super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer); @@ -110,13 +110,13 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul }, CLEANROOM(true, GameVersionNumber.between("1.12.2", "1.12.999"), VersionRange.all()) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return analyzer != null && analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM) && super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer); } @Override - public VersionRange getJavaVersionRange(Version version, LibraryAnalyzer analyzer) { + public VersionRange getJavaVersionRange(GameInstanceManifest manifest, LibraryAnalyzer analyzer) { if (analyzer == null || !analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) return VersionRange.all(); @@ -132,10 +132,10 @@ public VersionRange getJavaVersionRange(Version version, LibraryA // LaunchWrapper<=1.12 will crash because LaunchWrapper assumes the system class loader is an instance of URLClassLoader (Java 8) LAUNCH_WRAPPER(true, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { if (version == null) return false; - return super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer) && LAUNCH_WRAPPER_MAIN.equals(version.getMainClass()) && + return super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer) && LAUNCH_WRAPPER_MAIN.equals(version.mainClass()) && version.getLibraries().stream() .filter(library -> "launchwrapper".equals(library.getArtifactId())) .anyMatch(library -> VersionNumber.asVersion(library.getVersion()).compareTo(VersionNumber.asVersion("1.13")) < 0); @@ -147,7 +147,7 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul // For example, JDK 9+ 64-bit cannot load 32-bit lwjgl native library. VANILLA_LINUX_JAVA_8(true, GameVersionNumber.atMost("1.12.999"), VersionNumber.atMost("1.8.999")) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { return OperatingSystem.CURRENT_OS == OperatingSystem.LINUX && Architecture.SYSTEM_ARCH == Architecture.X86_64 @@ -156,14 +156,14 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul } @Override - public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) { + public boolean checkJava(GameVersionNumber gameVersionNumber, GameInstanceManifest version, JavaRuntime java, LibraryAnalyzer analyzer) { return java.getArchitecture() != Architecture.X86_64 || super.checkJava(gameVersionNumber, version, java, analyzer); } }, // Minecraft currently does not provide official support for architectures other than x86 and x86-64. VANILLA_X86(false, VersionRange.all(), VersionRange.all()) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { if (java == null || java.getArchitecture() != Architecture.ARM64) return false; @@ -175,14 +175,14 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul } @Override - public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) { + public boolean checkJava(GameVersionNumber gameVersionNumber, GameInstanceManifest version, JavaRuntime java, LibraryAnalyzer analyzer) { return java.getArchitecture().isX86(); } }, // Minecraft 1.16+Forge with crash because JDK-8273826 MODLAUNCHER_8(false, GameVersionNumber.between("1.16.3", "1.17.1"), VersionRange.all()) { @Override - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { if (version == null || java == null || analyzer == null || !super.appliesToVersionImpl(gameVersionNumber, version, java, analyzer)) return false; @@ -207,7 +207,7 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul } @Override - public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) { + public boolean checkJava(GameVersionNumber gameVersionNumber, GameInstanceManifest version, JavaRuntime java, LibraryAnalyzer analyzer) { int parsedJavaVersion = java.getParsedVersion(); if (parsedJavaVersion > 17) { return false; @@ -243,20 +243,20 @@ public VersionRange getGameVersionRange() { return gameVersionRange; } - public VersionRange getJavaVersionRange(Version version, LibraryAnalyzer analyzer) { + public VersionRange getJavaVersionRange(GameInstanceManifest manifest, LibraryAnalyzer analyzer) { return javaVersionRange; } - public final boolean appliesToVersion(@Nullable GameVersionNumber gameVersionNumber, @Nullable Version version, + public final boolean appliesToVersion(@Nullable GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, LibraryAnalyzer analyzer) { return gameVersionRange.contains(gameVersionNumber) && appliesToVersionImpl(gameVersionNumber, version, java, analyzer); } - protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable Version version, + protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nullable GameInstanceManifest version, @Nullable JavaRuntime java, @Nullable LibraryAnalyzer analyzer) { GameJavaVersion gameJavaVersion; - if (version == null || (gameJavaVersion = version.getJavaVersion()) == null) { + if (version == null || (gameJavaVersion = version.javaVersion()) == null) { return true; } @@ -271,7 +271,7 @@ protected boolean appliesToVersionImpl(GameVersionNumber gameVersionNumber, @Nul } @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public boolean checkJava(GameVersionNumber gameVersionNumber, Version version, JavaRuntime java, LibraryAnalyzer analyzer) { + public boolean checkJava(GameVersionNumber gameVersionNumber, GameInstanceManifest version, JavaRuntime java, LibraryAnalyzer analyzer) { return getJavaVersionRange(version, analyzer).contains(java.getVersionNumber()); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java index 309407a7607..c0866116813 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/LaunchOptions.java @@ -30,6 +30,7 @@ */ public class LaunchOptions implements Serializable { + private GameInstanceID instanceId; private Path gameDir; private JavaRuntime java; private String versionName; @@ -65,6 +66,10 @@ public class LaunchOptions implements Serializable { private boolean disableAutoGameOptions; private boolean daemon; + public GameInstanceID getInstanceId() { + return instanceId; + } + /** * The game directory */ @@ -311,6 +316,11 @@ public List getJavaAgents() { return options.javaAgents; } + public Builder setInstanceId(GameInstanceID instanceId) { + options.instanceId = instanceId; + return this; + } + public Builder setGameDir(Path gameDir) { options.gameDir = gameDir; return this; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java index dc253ca8e3a..6dd68a60f8c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Library.java @@ -17,18 +17,25 @@ */ package org.jackhuang.hmcl.game; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.JsonParseException; +import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.util.Constants; import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.ToStringBuilder; +import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.TolerableValidationException; import org.jackhuang.hmcl.util.gson.Validation; import org.jackhuang.hmcl.util.platform.Architecture; import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jetbrains.annotations.Nullable; +import java.lang.reflect.Type; import java.util.*; /** @@ -36,16 +43,13 @@ * * @author huangyuhui */ +@JsonAdapter(Library.Adapter.class) @Immutable public class Library implements Comparable, Validation { - /** - *

A possible native descriptors can be: [variant-]os[-key]

- * - *

- * Variant can be empty string, 'native', or 'natives'. - * Key can be empty string, system arch, or system arch bit count. - *

- */ + /// A possible native descriptors can be: [variant-]os[-key] + /// + /// Variant can be an empty string, 'native', or 'natives'. + /// Key can be an empty string, system arch, or system arch bit count. private static final String[] POSSIBLE_NATIVE_DESCRIPTORS; static { @@ -74,6 +78,55 @@ public class Library implements Comparable, Validation { } } + /// Parses a library from either the standard version JSON format or TLauncher library format. + public static Library fromJson(JsonObject json) { + if (json == null) { + throw new JsonParseException("Library must be a JSON object"); + } + + Artifact artifact = fromJsonMember(json, "name", Artifact.class); + String url = JsonUtils.getString(json, "url"); + LibrariesDownloadInfo downloads = fromJsonMember(json, "downloads", LibrariesDownloadInfo.class); + + if (downloads == null && (json.has("artifact") || json.has("classifies"))) { + LibraryDownloadInfo downloadArtifact = fromJsonMember(json, "artifact", LibraryDownloadInfo.class); + Map classifiers = fromJsonMember( + json, + "classifies", + JsonUtils.mapTypeOf(String.class, LibraryDownloadInfo.class).getType() + ); + downloads = new LibrariesDownloadInfo(downloadArtifact, classifiers); + } + + List checksums = fromJsonMember(json, "checksums", JsonUtils.listTypeOf(String.class).getType()); + ExtractRules extract = fromJsonMember(json, "extract", ExtractRules.class); + Map natives = fromJsonMember(json, "natives", JsonUtils.mapTypeOf(String.class, String.class).getType()); + List rules = fromJsonMember(json, "rules", JsonUtils.listTypeOf(CompatibilityRule.class).getType()); + + String hint = JsonUtils.getString(json, "hint"); + if (hint == null) { + hint = JsonUtils.getString(json, "MMC-hint"); + } + + String fileName = JsonUtils.getString(json, "filename"); + if (fileName == null) { + fileName = JsonUtils.getString(json, "MMC-filename"); + } + + return new Library(artifact, url, downloads, checksums, extract, natives, rules, hint, fileName); + } + + /// Reads a JSON member using HMCL's configured Gson instance. + private static @Nullable T fromJsonMember(JsonObject json, String name, Class type) { + return fromJsonMember(json, name, (Type) type); + } + + /// Reads a JSON member using HMCL's configured Gson instance. + private static @Nullable T fromJsonMember(JsonObject json, String name, Type type) { + JsonElement element = json.get(name); + return element == null || element.isJsonNull() ? null : JsonUtils.GSON.fromJson(element, type); + } + @SerializedName("name") private final Artifact artifact; private final String url; @@ -295,4 +348,18 @@ public void validate() throws JsonParseException, TolerableValidationException { if (artifact == null) throw new JsonParseException("Library.name cannot be null"); } + + /// Gson deserializer for library JSON variants. + public static final class Adapter implements JsonDeserializer { + @Override + public Library deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + if (json == null || json.isJsonNull()) + return null; + + if (json instanceof JsonObject jsonObject) + return fromJson(jsonObject); + else + throw new JsonParseException("Library must be a JSON object"); + } + } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionNotFoundException.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/NoSuchGameInstanceException.java similarity index 64% rename from HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionNotFoundException.java rename to HMCLCore/src/main/java/org/jackhuang/hmcl/game/NoSuchGameInstanceException.java index eaf669d0934..8e2c8a064d8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionNotFoundException.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/NoSuchGameInstanceException.java @@ -1,6 +1,6 @@ /* * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui and contributors + * Copyright (C) 2026 huangyuhui 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 @@ -17,20 +17,18 @@ */ package org.jackhuang.hmcl.game; -/** - * - * @author huangyuhui - */ -public final class VersionNotFoundException extends RuntimeException { +import org.jetbrains.annotations.NotNullByDefault; - public VersionNotFoundException() { - } +@NotNullByDefault +public class NoSuchGameInstanceException extends RuntimeException { + private final GameInstanceID id; - public VersionNotFoundException(String message) { - super(message); + public NoSuchGameInstanceException(GameInstanceID id) { + super(id.id()); + this.id = id; } - public VersionNotFoundException(String message, Throwable cause) { - super(message, cause); + public GameInstanceID getId() { + return id; } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/SimpleVersionProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/SimpleVersionProvider.java deleted file mode 100644 index 9dcec84c995..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/SimpleVersionProvider.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui 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 . - */ -package org.jackhuang.hmcl.game; - -import java.util.HashMap; -import java.util.Map; - -/** - * - * @author huangyuhui - */ -public class SimpleVersionProvider implements VersionProvider { - - protected final Map versionMap = new HashMap<>(); - - @Override - public boolean hasVersion(String id) { - return versionMap.containsKey(id); - } - - @Override - public Version getVersion(String id) { - if (!hasVersion(id)) - throw new VersionNotFoundException("Version id " + id + " not found"); - else - return versionMap.get(id); - } - - public void addVersion(Version version) { - versionMap.put(version.getId(), version); - } - - public Map getVersionMap() { - return versionMap; - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java deleted file mode 100644 index 26b037822c8..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/Version.java +++ /dev/null @@ -1,512 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui 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 . - */ -package org.jackhuang.hmcl.game; - -import com.google.gson.JsonParseException; -import org.jackhuang.hmcl.util.*; -import org.jackhuang.hmcl.util.gson.JsonMap; -import org.jackhuang.hmcl.util.gson.Validation; -import org.jetbrains.annotations.Nullable; - -import java.time.Instant; -import java.util.*; -import java.util.stream.Collectors; - -import static org.jackhuang.hmcl.util.logging.Logger.LOG; - -/** - * - * @author huangyuhui - */ -@Immutable -public class Version implements Comparable, Validation { - - /** - * Patches with higher priority can override info from other patches, such as mainClass. - */ - public static final int PRIORITY_MC = 0, PRIORITY_LOADER = 30000; - - private final String id; - private final String version; - private final Integer priority; - private final String minecraftArguments; - private final Arguments arguments; - private final String mainClass; - private final String inheritsFrom; - private final String jar; - private final AssetIndexInfo assetIndex; - private final String assets; - private final Integer complianceLevel; - @Nullable - private final GameJavaVersion javaVersion; - private final List libraries; - private final List compatibilityRules; - private final JsonMap downloads; - private final JsonMap logging; - private final ReleaseType type; - private final Instant time; - private final Instant releaseTime; - private final Integer minimumLauncherVersion; - private final Boolean root; - private final Boolean hidden; - private final List patches; - - private transient final boolean resolved; - - public Version(String id) { - this(false, id, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, false, true, null); - } - - /** - * Constructor for patch - * - * @param id patch id - * @param version patch version - * @param priority patch priority - * @param arguments patch additional arguments - * @param mainClass main class to override - * @param libraries additional libraries - */ - public Version(String id, String version, int priority, Arguments arguments, String mainClass, List libraries) { - this(false, id, version, priority, null, arguments, mainClass, null, null, null, null, null, null, libraries, null, null, null, null, null, null, null, null, null, null); - } - - public Version(boolean resolved, String id, String version, Integer priority, String minecraftArguments, Arguments arguments, String mainClass, String inheritsFrom, String jar, AssetIndexInfo assetIndex, String assets, Integer complianceLevel, GameJavaVersion javaVersion, List libraries, List compatibilityRules, Map downloads, Map logging, ReleaseType type, Instant time, Instant releaseTime, Integer minimumLauncherVersion, Boolean hidden, Boolean root, List patches) { - this.resolved = resolved; - this.id = id; - this.version = version; - this.priority = priority; - this.minecraftArguments = minecraftArguments; - this.arguments = arguments; - this.mainClass = mainClass; - this.inheritsFrom = inheritsFrom; - this.jar = jar; - this.assetIndex = assetIndex; - this.assets = assets; - this.complianceLevel = complianceLevel; - this.javaVersion = javaVersion; - this.libraries = Lang.copyList(libraries); - this.compatibilityRules = Lang.copyList(compatibilityRules); - this.downloads = downloads == null ? null : new JsonMap<>(downloads); - this.logging = logging == null ? null : new JsonMap<>(logging); - this.type = type; - this.time = time; - this.releaseTime = releaseTime; - this.minimumLauncherVersion = minimumLauncherVersion; - this.hidden = hidden; - this.root = root; - this.patches = Lang.copyList(patches); - } - - public Optional getMinecraftArguments() { - return Optional.ofNullable(minecraftArguments); - } - - public Optional getArguments() { - return Optional.ofNullable(arguments); - } - - public String getMainClass() { - return mainClass; - } - - public Instant getTime() { - return time; - } - - public String getId() { - return id; - } - - /** - * Version of the patch. - * Exists only when this version object represents a patch. - * Example: 0.5.0.33 for fabric-loader, 28.0.46 for minecraft-forge. - */ - @Nullable - public String getVersion() { - return version; - } - - public int getPriority() { - return priority == null ? Integer.MIN_VALUE : priority; - } - - public ReleaseType getType() { - return type == null ? ReleaseType.UNKNOWN : type; - } - - public Instant getReleaseTime() { - return releaseTime; - } - - public String getJar() { - return jar; - } - - public String getInheritsFrom() { - return inheritsFrom; - } - - public int getMinimumLauncherVersion() { - return minimumLauncherVersion == null ? 0 : minimumLauncherVersion; - } - - public Integer getComplianceLevel() { - return complianceLevel; - } - - public GameJavaVersion getJavaVersion() { - return javaVersion; - } - - public boolean isHidden() { - return hidden == null ? false : hidden; - } - - public boolean isRoot() { - return root == null ? false : root; - } - - public boolean isResolved() { - return resolved; - } - - public boolean isResolvedPreservingPatches() { - return inheritsFrom == null && !resolved; - } - - public List getPatches() { - return patches == null ? Collections.emptyList() : patches; - } - - public Map getLogging() { - return logging == null ? Collections.emptyMap() : Collections.unmodifiableMap(logging); - } - - public List getLibraries() { - return libraries == null ? Collections.emptyList() : Collections.unmodifiableList(libraries); - } - - public List getCompatibilityRules() { - return compatibilityRules == null ? Collections.emptyList() : Collections.unmodifiableList(compatibilityRules); - } - - public Map getDownloads() { - return downloads == null ? Collections.emptyMap() : Collections.unmodifiableMap(downloads); - } - - public DownloadInfo getDownloadInfo() { - DownloadInfo client = downloads == null ? null : downloads.get(DownloadType.CLIENT); - String jarName = jar == null ? id : jar; - if (client == null) - return new DownloadInfo(String.format("%s%s/%s.jar", Constants.DEFAULT_VERSION_DOWNLOAD_URL, jarName, jarName)); - else - return client; - } - - public AssetIndexInfo getAssetIndex() { - String assetsId = assets == null ? "legacy" : assets; - - if (assetIndex == null) { - String hash; - switch (assetsId) { - case "1.8": - hash = "f6ad102bcaa53b1a58358f16e376d548d44933ec"; - break; - case "14w31a": - hash = "10a2a0e75b03cfb5a7196abbdf43b54f7fa61deb"; - break; - case "14w25a": - hash = "32ff354a3be1c4dd83027111e6d79ee4d701d2c0"; - break; - case "1.7.4": - hash = "545510a60f526b9aa8a38f9c0bc7a74235d21675"; - break; - case "1.7.10": - hash = "1863782e33ce7b584fc45b037325a1964e095d3e"; - break; - case "1.7.3": - hash = "f6cf726f4747128d13887010c2cbc44ba83504d9"; - break; - case "pre-1.6": - hash = "3d8e55480977e32acd9844e545177e69a52f594b"; - break; - case "legacy": - default: - assetsId = "legacy"; - hash = "770572e819335b6c0a053f8378ad88eda189fc14"; - } - - String url = Constants.DEFAULT_INDEX_URL + hash + "/" + assetsId + ".json"; - return new AssetIndexInfo(assetsId, url); - } else { - return assetIndex; - } - } - - public boolean appliesToCurrentEnvironment() { - return CompatibilityRule.appliesToCurrentEnvironment(compatibilityRules); - } - - /** - * Resolve given version. - * Resolving version will list all patches within this version and its parents, - * which is for analysis. - */ - public Version resolve(VersionProvider provider) throws VersionNotFoundException { - if (isResolved()) return this; - return resolve(provider, new HashSet<>()).markAsResolved(); - } - - protected Version merge(Version parent, boolean isPatch) { - return new Version( - true, - id, - null, - null, - minecraftArguments == null ? parent.minecraftArguments : minecraftArguments, - Arguments.merge(parent.arguments, arguments), - mainClass == null ? parent.mainClass : mainClass, - null, // inheritsFrom - jar == null ? parent.jar : jar, - assetIndex == null ? parent.assetIndex : assetIndex, - assets == null ? parent.assets : assets, - complianceLevel, - javaVersion == null ? parent.javaVersion : javaVersion, - Lang.merge(this.libraries, parent.libraries), - Lang.merge(parent.compatibilityRules, this.compatibilityRules), - downloads == null ? parent.downloads : downloads, - logging == null ? parent.logging : logging, - type == null ? parent.type : type, - time == null ? parent.time : time, - releaseTime == null ? parent.releaseTime : releaseTime, - Lang.merge(minimumLauncherVersion, parent.minimumLauncherVersion, Math::max), - hidden, - true, - isPatch ? parent.patches : Lang.merge(Lang.merge(parent.patches, Collections.singleton(toPatch())), patches)); - } - - protected Version resolve(VersionProvider provider, Set resolvedSoFar) throws VersionNotFoundException { - Version thisVersion; - - if (inheritsFrom == null) { - if (isRoot()) { - // TODO: Breaking change, require much testing on versions installed with external installer, other launchers, and all kinds of versions. - thisVersion = patches != null ? new Version(id).setPatches(patches) : this; - } else { - thisVersion = this; - } - thisVersion = this.jar == null ? thisVersion.setJar(id) : thisVersion.setJar(this.jar); - } else { - // To maximize the compatibility. - if (!resolvedSoFar.add(id)) { - LOG.warning("Found circular dependency versions: " + resolvedSoFar); - thisVersion = this.jar == null ? this.setJar(id) : this; - } else { - // It is supposed to auto install an version in getVersion. - thisVersion = merge(provider.getVersion(inheritsFrom).resolve(provider, resolvedSoFar), false); - } - } - - if (patches == null) { - // This is a version from external launcher. NO need to resolve the patches. - return thisVersion; - } else if (!patches.isEmpty()) { - // Assume patches themselves do not have patches recursively. - List sortedPatches = patches.stream() - .sorted(Comparator.comparing(Version::getPriority)) - .collect(Collectors.toList()); - for (Version patch : sortedPatches) { - thisVersion = patch.setJar(null).merge(thisVersion, true); - } - } - - return thisVersion.setId(id); - } - - private Version toPatch() { - return this.clearPatches().setHidden(true).setId("resolved." + getId()); - } - - /** - * Resolve the version preserving all dependencies and patches. - */ - public Version resolvePreservingPatches(VersionProvider provider) throws VersionNotFoundException { - return resolvePreservingPatches(provider, new HashSet<>()); - } - - protected Version mergePreservingPatches(Version parent) { - return parent.addPatch(toPatch()).addPatches(patches); - } - - protected Version resolvePreservingPatches(VersionProvider provider, Set resolvedSoFar) throws VersionNotFoundException { - Version thisVersion = isRoot() ? this : new Version(id).addPatch(toPatch()).addPatches(getPatches()); - - if (inheritsFrom == null) { - // keep thisVersion - } else { - // To maximize the compatibility. - if (!resolvedSoFar.add(id)) { - LOG.warning("Found circular dependency versions: " + resolvedSoFar); - // keep thisVersion - } else { - // It is supposed to auto install an version in getVersion. - thisVersion = mergePreservingPatches(provider.getVersion(inheritsFrom).resolvePreservingPatches(provider, resolvedSoFar)); - } - } - - return thisVersion.setId(id).setJar(resolve(provider).getJar()); - } - - public Version markAsResolved() { - return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version markAsUnresolved() { - return new Version(false, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - private Version setHidden(Boolean hidden) { - return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setRoot(Boolean root) { - return new Version(true, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setId(String id) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setVersion(String version) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setPriority(Integer priority) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setMinecraftArguments(String minecraftArguments) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setJavaVersion(GameJavaVersion javaVersion) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setArguments(Arguments arguments) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setMainClass(String mainClass) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setInheritsFrom(String inheritsFrom) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setJar(String jar) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setAssetIndex(AssetIndexInfo assetIndex) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setLibraries(List libraries) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setDownload(JsonMap downloads) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setLogging(Map logging) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version setPatches(List patches) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version addPatch(Version... additional) { - return addPatches(Arrays.asList(additional)); - } - - public Version addPatches(@Nullable List additional) { - Set patchIds = additional == null ? Collections.emptySet() : additional.stream().map(Version::getId).collect(Collectors.toSet()); - List patches = Lang.merge(this.patches == null ? null : this.patches.stream().filter(patch -> !patchIds.contains(patch.getId())).collect(Collectors.toList()), additional); - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, patches); - } - - public Version clearPatches() { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, null); - } - - public Version removePatchById(String patchId) { - return new Version(resolved, id, version, priority, minecraftArguments, arguments, mainClass, inheritsFrom, jar, assetIndex, assets, complianceLevel, javaVersion, libraries, compatibilityRules, downloads, logging, type, time, releaseTime, minimumLauncherVersion, hidden, root, - patches == null ? null : patches.stream().filter(patch -> !patchId.equals(patch.getId())).collect(Collectors.toList())); - } - - public boolean hasPatch(String patchId) { - return patches != null && patches.stream().anyMatch(patch -> patchId.equals(patch.getId())); - } - - @Override - public int hashCode() { - return id.hashCode(); - } - - @Override - public boolean equals(Object obj) { - return obj instanceof Version && Objects.equals(id, ((Version) obj).id); - } - - @Override - public int compareTo(Version o) { - return id.compareTo(o.id); - } - - @Override - public String toString() { - return new ToStringBuilder(this).append("id", id).toString(); - } - - @Override - public void validate() throws JsonParseException { - if (StringUtils.isBlank(id)) - throw new JsonParseException("Version ID cannot be blank"); - if (downloads != null) - for (Map.Entry entry : downloads.entrySet()) { - if (!(entry.getKey() instanceof DownloadType)) - throw new JsonParseException("Version downloads key must be DownloadType"); - if (!(entry.getValue() instanceof DownloadInfo)) - throw new JsonParseException("Version downloads value must be DownloadInfo"); - } - if (logging != null) - for (Map.Entry entry : logging.entrySet()) { - if (!(entry.getKey() instanceof DownloadType)) - throw new JsonParseException("Version logging key must be DownloadType"); - if (!(entry.getValue() instanceof LoggingInfo)) - throw new JsonParseException("Version logging value must be LoggingInfo"); - } - } - -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionJsonType.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionJsonType.java deleted file mode 100644 index 589200fe627..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionJsonType.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui 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 . - */ -package org.jackhuang.hmcl.game; - -public enum VersionJsonType { - OFFICIAL, - TLAUNCHER -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionProvider.java deleted file mode 100644 index 48fadf764ee..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/VersionProvider.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui 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 . - */ -package org.jackhuang.hmcl.game; - -/** - * Supports version accessing. - * - * @see Version#resolve - * @author huangyuhui - */ -public interface VersionProvider { - - /** - * Does the version of id exist? - * - * @param id the id of version - * @return true if the version exists - */ - boolean hasVersion(String id); - - /** - * Get the version - * - * @param id the id of version - * @return the version you want - */ - Version getVersion(String id) throws VersionNotFoundException; -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherLibrary.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherLibrary.java deleted file mode 100644 index 9bf55a366a1..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherLibrary.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui 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 . - */ -package org.jackhuang.hmcl.game.tlauncher; - -import com.google.gson.annotations.SerializedName; -import org.jackhuang.hmcl.game.Artifact; -import org.jackhuang.hmcl.game.CompatibilityRule; -import org.jackhuang.hmcl.game.ExtractRules; -import org.jackhuang.hmcl.game.LibrariesDownloadInfo; -import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.LibraryDownloadInfo; -import org.jackhuang.hmcl.util.Immutable; -import org.jackhuang.hmcl.util.gson.JsonSerializable; - -import java.util.List; -import java.util.Map; - -@Immutable -@JsonSerializable -public final class TLauncherLibrary { - - @SerializedName("name") - private final Artifact name; - private final String url; - private final LibraryDownloadInfo artifact; - - @SerializedName("classifies") // stupid typo made by TLauncher - private final Map classifiers; - private final ExtractRules extract; - private final Map natives; - private final List rules; - private final List checksums; - - public TLauncherLibrary(Artifact name, String url, LibraryDownloadInfo artifact, Map classifiers, ExtractRules extract, Map natives, List rules, List checksums) { - this.name = name; - this.url = url; - this.artifact = artifact; - this.classifiers = classifiers; - this.extract = extract; - this.natives = natives; - this.rules = rules; - this.checksums = checksums; - } - - public Library toLibrary() { - return new Library( - name, - url, - new LibrariesDownloadInfo(artifact, classifiers), - checksums, - extract, - natives, - rules, - null, - null - ); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherVersion.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherVersion.java deleted file mode 100644 index d3d5095d5dc..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/game/tlauncher/TLauncherVersion.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2021 huangyuhui 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 . - */ -package org.jackhuang.hmcl.game.tlauncher; - -import com.google.gson.JsonParseException; -import org.jackhuang.hmcl.game.*; -import org.jackhuang.hmcl.util.gson.JsonMap; -import org.jackhuang.hmcl.util.gson.TolerableValidationException; -import org.jackhuang.hmcl.util.gson.Validation; -import org.jetbrains.annotations.Nullable; - -import java.time.Instant; -import java.util.List; -import java.util.stream.Collectors; - -public final class TLauncherVersion implements Validation { - - private final String id; - private final String minecraftArguments; - private final Arguments arguments; - private final String mainClass; - private final String inheritsFrom; - private final String jar; - private final AssetIndexInfo assetIndex; - private final String assets; - private final Integer complianceLevel; - @Nullable - private final GameJavaVersion javaVersion; - private final List libraries; - private final List compatibilityRules; - private final JsonMap downloads; - private final JsonMap logging; - private final ReleaseType type; - private final Instant time; - private final Instant releaseTime; - private final Integer minimumLauncherVersion; - private final Integer tlauncherVersion; - - public TLauncherVersion(String id, String minecraftArguments, Arguments arguments, String mainClass, String inheritsFrom, String jar, AssetIndexInfo assetIndex, String assets, Integer complianceLevel, @Nullable GameJavaVersion javaVersion, List libraries, List compatibilityRules, JsonMap downloads, JsonMap logging, ReleaseType type, Instant time, Instant releaseTime, Integer minimumLauncherVersion, Integer tlauncherVersion) { - this.id = id; - this.minecraftArguments = minecraftArguments; - this.arguments = arguments; - this.mainClass = mainClass; - this.inheritsFrom = inheritsFrom; - this.jar = jar; - this.assetIndex = assetIndex; - this.assets = assets; - this.complianceLevel = complianceLevel; - this.javaVersion = javaVersion; - this.libraries = libraries; - this.compatibilityRules = compatibilityRules; - this.downloads = downloads; - this.logging = logging; - this.type = type; - this.time = time; - this.releaseTime = releaseTime; - this.minimumLauncherVersion = minimumLauncherVersion; - this.tlauncherVersion = tlauncherVersion; - } - - @Override - public void validate() throws JsonParseException, TolerableValidationException { - Validation.requireNonNull(tlauncherVersion, "Not TLauncher version json format"); - } - - public Version toVersion() { - return new Version( - false, - id, - null, - null, - minecraftArguments, - arguments, - mainClass, - inheritsFrom, - jar, - assetIndex, - assets, - complianceLevel, - javaVersion, - libraries == null ? null : libraries.stream().map(TLauncherLibrary::toLibrary).collect(Collectors.toList()), - compatibilityRules, - downloads, - logging, - type, - time, - releaseTime, - minimumLauncherVersion, - null, - null, - null - ); - } -} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java index 1cdf87d2566..4ef8e3af8bd 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/DefaultLauncher.java @@ -52,18 +52,18 @@ public class DefaultLauncher extends Launcher { private final LibraryAnalyzer analyzer; - public DefaultLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options) { - this(repository, version, authInfo, options, null); + public DefaultLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options) { + this(repository, manifest, authInfo, options, null); } - public DefaultLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) { - this(repository, version, authInfo, options, listener, true); + public DefaultLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) { + this(repository, manifest, authInfo, options, listener, true); } - public DefaultLauncher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) { - super(repository, version, authInfo, options, listener, daemon); + public DefaultLauncher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) { + super(repository, manifest, authInfo, options, listener, daemon); - this.analyzer = LibraryAnalyzer.analyze(version, repository.getGameVersion(version).orElse(null)); + this.analyzer = LibraryAnalyzer.analyze(manifest, repository.getGameVersion(manifest).orElse(null)); } private Command generateCommandLine(Path nativeFolder) throws IOException { @@ -158,11 +158,11 @@ private Command generateCommandLine(Path nativeFolder) throws IOException { if (!options.isNoGeneratedJVMArgs()) { appendJvmArgs(res); - res.addDefault("-Dminecraft.client.jar=", FileUtils.getAbsolutePath(repository.getVersionJar(version))); + res.addDefault("-Dminecraft.client.jar=", FileUtils.getAbsolutePath(repository.getInstanceJar(manifest))); if (OperatingSystem.CURRENT_OS == OperatingSystem.MACOS) { - res.addDefault("-Xdock:name=", "Minecraft " + version.getId()); - repository.getAssetObject(version.getId(), version.getAssetIndex().getId(), "icons/minecraft.icns") + res.addDefault("-Xdock:name=", "Minecraft " + manifest.id()); + repository.getAssetObject(manifest.id(), manifest.getAssetIndex().getId(), "icons/minecraft.icns") .ifPresent(minecraftIcns -> { res.addDefault("-Xdock:icon=", FileUtils.getAbsolutePath(minecraftIcns)); }); @@ -281,25 +281,25 @@ private Command generateCommandLine(Path nativeFolder) throws IOException { } } - Set classpath = repository.getClasspath(version); + Set classpath = repository.getClasspath(manifest); if (analyzer.has(LibraryAnalyzer.LibraryType.CLEANROOM)) { classpath.removeIf(c -> c.contains("2.9.4-nightly-20150209")); } - Path jar = repository.getVersionJar(version); + Path jar = repository.getInstanceJar(manifest); if (!Files.isRegularFile(jar)) throw new IOException("Minecraft jar does not exist"); classpath.add(FileUtils.getAbsolutePath(jar.toAbsolutePath())); // Provided Minecraft arguments - Path gameAssets = repository.getActualAssetDirectory(version.getId(), version.getAssetIndex().getId()); + Path gameAssets = repository.getActualAssetDirectory(manifest.id(), manifest.getAssetIndex().getId()); Map configuration = getConfigurations(); configuration.put("${classpath}", String.join(File.pathSeparator, classpath)); configuration.put("${game_assets}", FileUtils.getAbsolutePath(gameAssets)); configuration.put("${assets_root}", FileUtils.getAbsolutePath(gameAssets)); - Optional gameVersion = repository.getGameVersion(version); + Optional gameVersion = repository.getGameVersion(manifest); // lwjgl assumes path to native libraries encoded by ASCII. // Here is a workaround for this issue: https://github.com/HMCL-dev/HMCL/issues/1141. @@ -314,7 +314,7 @@ private Command generateCommandLine(Path nativeFolder) throws IOException { configuration.put("${natives_directory}", nativeFolderPath); Path javaNativeFolder = FileUtils.toAbsolute(nativeFolder); - @Nullable List jvmArguments = version.getArguments().map(Arguments::jvm).orElse(null); + @Nullable List jvmArguments = Optional.ofNullable(manifest.arguments()).map(Arguments::jvm).orElse(null); if (jvmArguments != null) { for (Argument jvmArgument : jvmArguments) { @@ -351,17 +351,17 @@ private Command generateCommandLine(Path nativeFolder) throws IOException { res.add("-javaagent:" + javaAgent); } - if (version.getMainClass() == null) { - throw new IllegalStateException("Main class is null for instance " + version.getId()); + if (manifest.mainClass() == null) { + throw new IllegalStateException("Main class is null for instance " + manifest.id()); } - res.add(version.getMainClass()); + res.add(manifest.mainClass()); - res.addAll(Arguments.parseStringArguments(version.getMinecraftArguments().map(StringUtils::tokenize).orElseGet(ArrayList::new), configuration)); + res.addAll(Arguments.parseStringArguments(Optional.ofNullable(manifest.minecraftArguments()).map(StringUtils::tokenize).orElseGet(ArrayList::new), configuration)); Map features = getFeatures(); - version.getArguments().map(Arguments::game).ifPresent(arguments -> res.addAll(Arguments.parseArguments(arguments, configuration, features))); - if (version.getMinecraftArguments().isPresent()) { + Optional.ofNullable(manifest.arguments()).map(Arguments::game).ifPresent(arguments -> res.addAll(Arguments.parseArguments(arguments, configuration, features))); + if (Optional.ofNullable(manifest.minecraftArguments()).isPresent()) { res.addAll(Arguments.parseArguments(this.getDefaultGameArguments(), configuration, features)); } if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.game() != null && !argumentsFromAuthInfo.game().isEmpty()) @@ -461,9 +461,9 @@ public void decompressNatives(Path destination) throws NotDecompressingNativesEx try { FileUtils.cleanDirectoryQuietly(destination); - for (Library library : version.getLibraries()) + for (Library library : manifest.getLibraries()) if (library.isNative()) - new Unzipper(repository.getLibraryFile(version, library), destination) + new Unzipper(repository.getLibraryFile(manifest, library), destination) .setFilter((zipEntry, destFile, relativePath) -> { if (!zipEntry.isDirectory() && !zipEntry.isUnixSymlink() && Files.isRegularFile(destFile) @@ -490,11 +490,11 @@ public void decompressNatives(Path destination) throws NotDecompressingNativesEx } private boolean isUsingLog4j() { - return GameVersionNumber.compare(repository.getGameVersion(version).orElse("1.7"), "1.7") >= 0; + return GameVersionNumber.compare(repository.getGameVersion(manifest).orElse("1.7"), "1.7") >= 0; } public Path getLog4jConfigurationFile() { - return repository.getVersionRoot(version.getId()).resolve("log4j2.xml"); + return repository.getInstanceRoot(manifest.id()).resolve("log4j2.xml"); } public void extractLog4jConfigurationFile() throws IOException { @@ -502,7 +502,7 @@ public void extractLog4jConfigurationFile() throws IOException { String sourcePath; - if (GameVersionNumber.asGameVersion(repository.getGameVersion(version)).compareTo("1.12") < 0) { + if (GameVersionNumber.asGameVersion(repository.getGameVersion(manifest)).compareTo("1.12") < 0) { if (options.isEnableDebugLogOutput()) { sourcePath = "/assets/game/log4j2-1.7-debug.xml"; } else { @@ -528,35 +528,35 @@ protected Map getConfigurations() { pair("${auth_session}", authInfo.getAccessToken()), pair("${auth_access_token}", authInfo.getAccessToken()), pair("${auth_uuid}", UUIDs.toCompactString(authInfo.getUUID())), - pair("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(version.getId())), + pair("${version_name}", Optional.ofNullable(options.getVersionName()).orElse(manifest.id().toString())), pair("${profile_name}", Optional.ofNullable(options.getProfileName()).orElse("Minecraft")), - pair("${version_type}", Optional.ofNullable(options.getVersionType()).orElse(version.getType().getId())), - pair("${game_directory}", FileUtils.getAbsolutePath(repository.getRunDirectory(version.getId()))), + pair("${version_type}", Optional.ofNullable(options.getVersionType()).orElse(manifest.type() != null ? manifest.type().getId() : ReleaseType.UNKNOWN.getId())), + pair("${game_directory}", FileUtils.getAbsolutePath(repository.getRunDirectory(manifest.id()))), pair("${user_type}", authInfo.getUserType()), - pair("${assets_index_name}", version.getAssetIndex().getId()), + pair("${assets_index_name}", manifest.getAssetIndex().getId()), pair("${user_properties}", authInfo.getUserProperties()), pair("${resolution_width}", options.getWidth().toString()), pair("${resolution_height}", options.getHeight().toString()), - pair("${library_directory}", FileUtils.getAbsolutePath(repository.getLibrariesDirectory(version))), + pair("${library_directory}", FileUtils.getAbsolutePath(repository.getLibrariesDirectory(manifest))), pair("${classpath_separator}", File.pathSeparator), - pair("${primary_jar}", FileUtils.getAbsolutePath(repository.getVersionJar(version))), + pair("${primary_jar}", FileUtils.getAbsolutePath(repository.getInstanceJar(manifest))), pair("${language}", Locale.getDefault().toLanguageTag()), // defined by HMCL // libraries_directory stands for historical reasons here. We don't know the official launcher // had already defined "library_directory" as the placeholder for path to ".minecraft/libraries" // when we propose this placeholder. - pair("${libraries_directory}", FileUtils.getAbsolutePath(repository.getLibrariesDirectory(version))), + pair("${libraries_directory}", FileUtils.getAbsolutePath(repository.getLibrariesDirectory(manifest))), // file_separator is used in -DignoreList pair("${file_separator}", File.separator), - pair("${primary_jar_name}", FileUtils.getName(repository.getVersionJar(version))) + pair("${primary_jar_name}", FileUtils.getName(repository.getInstanceJar(manifest))) ); } /// Returns the native library directory selected by the launch options. private Path getNativeFolder() { if (StringUtils.isBlank(options.getNativesDir())) { - return repository.getNativeDirectory(version.getId(), options.getJava().getPlatform()); + return repository.getNativeDirectory(manifest.id(), options.getJava().getPlatform()); } return Path.of(options.getNativesDir()); @@ -587,7 +587,7 @@ public ManagedProcess launch() throws IOException, InterruptedException { if (isUsingLog4j()) extractLog4jConfigurationFile(); - Path runDirectory = repository.getRunDirectory(version.getId()); + Path runDirectory = repository.getRunDirectory(manifest.id()); if (StringUtils.isNotBlank(options.getPreLaunchCommand())) { ProcessBuilder builder = new ProcessBuilder(StringUtils.tokenize(options.getPreLaunchCommand(), getEnvVars(nativeFolder))).directory(runDirectory.toFile()); @@ -617,13 +617,13 @@ public ManagedProcess launch() throws IOException, InterruptedException { } private Map getEnvVars(Path nativeFolder) { - String versionName = Optional.ofNullable(options.getVersionName()).orElse(version.getId()); + String versionName = Optional.ofNullable(options.getVersionName()).orElse(manifest.id().toString()); Map env = new LinkedHashMap<>(); env.put("INST_NAME", versionName); env.put("INST_ID", versionName); - env.put("INST_DIR", FileUtils.getAbsolutePath(repository.getVersionRoot(version.getId()))); - env.put("INST_MC_DIR", FileUtils.getAbsolutePath(repository.getRunDirectory(version.getId()))); + env.put("INST_DIR", FileUtils.getAbsolutePath(repository.getInstanceRoot(manifest.id()))); + env.put("INST_MC_DIR", FileUtils.getAbsolutePath(repository.getRunDirectory(manifest.id()))); env.put("INST_JAVA", options.getJava().getBinary().toString()); if (options.getRenderer() instanceof Renderer.Driver driver) { @@ -782,7 +782,7 @@ else if (!isWindows && !(scriptExtension.equalsIgnoreCase("sh") || scriptExtensi writer.newLine(); } writer.write("Set-Location -LiteralPath "); - writer.write(CommandBuilder.pwshString(FileUtils.getAbsolutePath(repository.getRunDirectory(version.getId())))); + writer.write(CommandBuilder.pwshString(FileUtils.getAbsolutePath(repository.getRunDirectory(manifest.id())))); writer.newLine(); @@ -826,7 +826,7 @@ else if (!isWindows && !(scriptExtension.equalsIgnoreCase("sh") || scriptExtensi writer.newLine(); } writer.newLine(); - writer.write(new CommandBuilder().addAll("cd", "/D", FileUtils.getAbsolutePath(repository.getRunDirectory(version.getId()))).toString()); + writer.write(new CommandBuilder().addAll("cd", "/D", FileUtils.getAbsolutePath(repository.getRunDirectory(manifest.id()))).toString()); } else { writer.write("#!/usr/bin/env bash"); writer.newLine(); @@ -838,7 +838,7 @@ else if (!isWindows && !(scriptExtension.equalsIgnoreCase("sh") || scriptExtensi writer.write(new CommandBuilder().addAll("ln", "-s", FileUtils.getAbsolutePath(nativeFolder), commandLine.tempNativeFolder.toString()).toString()); writer.newLine(); } - writer.write(new CommandBuilder().addAll("cd", FileUtils.getAbsolutePath(repository.getRunDirectory(version.getId()))).toString()); + writer.write(new CommandBuilder().addAll("cd", FileUtils.getAbsolutePath(repository.getRunDirectory(manifest.id()))).toString()); } writer.newLine(); if (StringUtils.isNotBlank(options.getPreLaunchCommand())) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Launcher.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Launcher.java index e4a97f9f795..c20c05a8996 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Launcher.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/launch/Launcher.java @@ -18,9 +18,9 @@ package org.jackhuang.hmcl.launch; import org.jackhuang.hmcl.auth.AuthInfo; +import org.jackhuang.hmcl.game.GameInstanceManifest; import org.jackhuang.hmcl.game.GameRepository; import org.jackhuang.hmcl.game.LaunchOptions; -import org.jackhuang.hmcl.game.Version; import org.jackhuang.hmcl.util.platform.ManagedProcess; import java.io.IOException; @@ -33,23 +33,23 @@ public abstract class Launcher { protected final GameRepository repository; - protected final Version version; + protected final GameInstanceManifest manifest; protected final AuthInfo authInfo; protected final LaunchOptions options; protected final ProcessListener listener; protected final boolean daemon; - public Launcher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options) { - this(repository, version, authInfo, options, null); + public Launcher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options) { + this(repository, manifest, authInfo, options, null); } - public Launcher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) { - this(repository, version, authInfo, options, listener, true); + public Launcher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener) { + this(repository, manifest, authInfo, options, listener, true); } - public Launcher(GameRepository repository, Version version, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) { + public Launcher(GameRepository repository, GameInstanceManifest manifest, AuthInfo authInfo, LaunchOptions options, ProcessListener listener, boolean daemon) { this.repository = repository; - this.version = version; + this.manifest = manifest; this.authInfo = authInfo; this.options = options; this.listener = listener; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/Modpack.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/Modpack.java index b6a60f699f5..d0dd38d9f52 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/Modpack.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/Modpack.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.modpack; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.task.Task; import java.nio.charset.Charset; @@ -119,7 +120,7 @@ public Modpack setManifest(ModpackManifest manifest) { return this; } - public abstract Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl); + public abstract Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl); public static boolean acceptFile(String path, List blackList, List whiteList) { if (path.isEmpty()) diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/ModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/ModpackProvider.java index f3a516ce3d5..014447f9369 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/ModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/ModpackProvider.java @@ -20,6 +20,7 @@ import com.google.gson.JsonParseException; import kala.compress.archivers.zip.ZipArchiveReader; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.LaunchOptions; import org.jackhuang.hmcl.task.Task; @@ -31,9 +32,9 @@ public interface ModpackProvider { String getName(); - Task createCompletionTask(DefaultDependencyManager dependencyManager, String version); + Task createCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId); - Task createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException; + Task createUpdateTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException; /** * @param zipFile the opened modpack zip file. diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/ModpackUpdateTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/ModpackUpdateTask.java index 9e101f5d4af..f06f08a7267 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/ModpackUpdateTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/ModpackUpdateTask.java @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.modpack; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.io.FileUtils; @@ -29,20 +30,20 @@ public class ModpackUpdateTask extends Task { private final DefaultGameRepository repository; - private final String id; + private final GameInstanceID id; private final Task updateTask; private final Path backupFolder; - public ModpackUpdateTask(DefaultGameRepository repository, String id, Task updateTask) { + public ModpackUpdateTask(DefaultGameRepository repository, GameInstanceID instanceId, Task updateTask) { this.repository = repository; - this.id = id; + this.id = instanceId; this.updateTask = updateTask; Path backup = repository.getBaseDirectory().resolve("backup"); while (true) { int num = (int)(Math.random() * 10000000); - if (!Files.exists(backup.resolve(id + "-" + num))) { - backupFolder = backup.resolve(id + "-" + num); + if (!Files.exists(backup.resolve(instanceId + "-" + num))) { + backupFolder = backup.resolve(instanceId + "-" + num); break; } } @@ -55,7 +56,7 @@ public Collection> getDependencies() { @Override public void execute() throws Exception { - FileUtils.copyDirectory(repository.getVersionRoot(id), backupFolder); + FileUtils.copyDirectory(repository.getInstanceRoot(id), backupFolder); } @Override @@ -69,11 +70,11 @@ public void postExecute() throws Exception { // Keep backup game version for further repair. } else { // Restore backup - repository.removeVersionFromDisk(id); + repository.removeInstanceFromDisk(id); - FileUtils.copyDirectory(backupFolder, repository.getVersionRoot(id)); + FileUtils.copyDirectory(backupFolder, repository.getInstanceRoot(id)); - repository.refreshVersionsAsync().start(); + repository.refreshAsync().start(); } } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java index 5885be26fe5..a26fd6d9f6a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseCompletionTask.java @@ -23,6 +23,7 @@ import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.addon.mod.ModManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.ModpackCompletionException; import org.jackhuang.hmcl.addon.RemoteAddon; import org.jackhuang.hmcl.task.FileDownloadTask; @@ -53,7 +54,7 @@ public final class CurseCompletionTask extends Task { private final DefaultDependencyManager dependency; private final DefaultGameRepository repository; private final ModManager modManager; - private final String version; + private final GameInstanceID instanceId; private CurseManifest manifest; private List> dependencies; @@ -65,29 +66,29 @@ public final class CurseCompletionTask extends Task { * Constructor. * * @param dependencyManager the dependency manager. - * @param version the existent and physical version. + * @param instanceId the existent and physical version. */ - public CurseCompletionTask(DefaultDependencyManager dependencyManager, String version) { - this(dependencyManager, version, null); + public CurseCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + this(dependencyManager, instanceId, null); } /** * Constructor. * * @param dependencyManager the dependency manager. - * @param version the existent and physical version. + * @param instanceId the existent and physical version. * @param manifest the CurseForgeModpack manifest. */ - public CurseCompletionTask(DefaultDependencyManager dependencyManager, String version, CurseManifest manifest) { + public CurseCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, CurseManifest manifest) { this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); - this.modManager = repository.getModManager(version); - this.version = version; + this.modManager = repository.getModManager(instanceId); + this.instanceId = instanceId; this.manifest = manifest; if (manifest == null) try { - Path manifestFile = repository.getVersionRoot(version).resolve("manifest.json"); + Path manifestFile = repository.getInstanceRoot(instanceId).resolve("manifest.json"); if (Files.exists(manifestFile)) this.manifest = JsonUtils.fromJsonFile(manifestFile, CurseManifest.class); } catch (Exception e) { @@ -112,7 +113,7 @@ public void execute() throws Exception { if (manifest == null) return; - Path root = repository.getVersionRoot(version); + Path root = repository.getInstanceRoot(instanceId); // Because in China, Curse is too difficult to visit, // if failed, ignore it and retry next time. @@ -140,7 +141,7 @@ public void execute() throws Exception { .collect(Collectors.toList())); JsonUtils.writeToJsonFile(root.resolve("manifest.json"), newManifest); - Path versionRoot = repository.getVersionRoot(modManager.getInstanceId()); + Path versionRoot = repository.getInstanceRoot(modManager.getInstanceId()); Path resourcePacksRoot = versionRoot.resolve("resourcepacks"); Path shaderPacksRoot = versionRoot.resolve("shaderpacks"); finished.set(0); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java index b8cf50d8f2d..c5da9fa47fe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseInstallTask.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.*; import org.jackhuang.hmcl.task.CacheFileTask; import org.jackhuang.hmcl.task.Task; @@ -50,7 +51,7 @@ public final class CurseInstallTask extends Task { private final Path zipFile; private final Modpack modpack; private final CurseManifest manifest; - private final String name; + private final GameInstanceID instanceId; private final String iconUrl; private final Path run; private final ModpackConfiguration config; @@ -67,21 +68,24 @@ public final class CurseInstallTask extends Task { * @param manifest The manifest content of given CurseForge modpack. * @param name the new version name */ - public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, CurseManifest manifest, String name, String iconUrl) { + public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, CurseManifest manifest, GameInstanceID name, String iconUrl) { this.dependencyManager = dependencyManager; this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; - this.name = name; + this.instanceId = name; this.iconUrl = iconUrl; this.repository = dependencyManager.getGameRepository(); - this.run = repository.getRunDirectory(name); - Path json = repository.getModpackConfiguration(name); - if (repository.hasVersion(name) && Files.notExists(json)) - throw new IllegalArgumentException("Version " + name + " already exists."); + GameInstanceID instanceId = name; - GameBuilder builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.minecraft().gameVersion()); + this.run = repository.getRunDirectory(instanceId); + + Path json = repository.getModpackConfiguration(instanceId); + if (repository.hasInstance(instanceId) && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " already exists."); + + GameBuilder builder = dependencyManager.newGameBuilder().name(instanceId).gameVersion(manifest.minecraft().gameVersion()); for (CurseManifestModLoader modLoader : manifest.minecraft().modLoaders()) { if (modLoader.id().startsWith("forge-")) { builder.version("forge", modLoader.id().substring("forge-".length())); @@ -97,7 +101,7 @@ public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile Exception ex = event.getTask().getException(); if (event.isFailed()) { if (!(ex instanceof ModpackCompletionException)) { - repository.removeVersionFromDisk(name); + repository.removeInstanceFromDisk(name); } } }); @@ -124,7 +128,7 @@ public CurseInstallTask(DefaultDependencyManager dependencyManager, Path zipFile dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl))); } } - dependencies.add(new CurseCompletionTask(dependencyManager, name, manifest)); + dependencies.add(new CurseCompletionTask(dependencyManager, instanceId, manifest)); } @Override @@ -171,7 +175,7 @@ public void execute() throws Exception { // CurseForge manifest where fileName is missing. CurseCompletionTask // resolves those file names and writes the enriched manifest to // manifest.json, so read from there when available. - Path oldManifestFile = repository.getVersionRoot(name).resolve("manifest.json"); + Path oldManifestFile = repository.getInstanceRoot(instanceId).resolve("manifest.json"); List oldFiles = config.getManifest().files(); if (Files.exists(oldManifestFile)) { try { @@ -195,7 +199,7 @@ public void execute() throws Exception { } } - Path root = repository.getVersionRoot(name); + Path root = repository.getInstanceRoot(instanceId); Files.createDirectories(root); JsonUtils.writeToJsonFile(root.resolve("manifest.json"), manifest); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseModpackProvider.java index 00cb8ce912b..f66aef4f19a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/curse/CurseModpackProvider.java @@ -21,6 +21,7 @@ import kala.compress.archivers.zip.ZipArchiveEntry; import kala.compress.archivers.zip.ZipArchiveReader; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackProvider; @@ -43,16 +44,16 @@ public String getName() { } @Override - public Task createCompletionTask(DefaultDependencyManager dependencyManager, String version) { - return new CurseCompletionTask(dependencyManager, version); + public Task createCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + return new CurseCompletionTask(dependencyManager, instanceId); } @Override - public Task createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { + public Task createUpdateTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { if (!(modpack.getManifest() instanceof CurseManifest curseManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, name, null)); + return new ModpackUpdateTask(dependencyManager.getGameRepository(), instanceId, new CurseInstallTask(dependencyManager, zipFile, modpack, curseManifest, instanceId, null)); } @Override @@ -68,8 +69,8 @@ public Modpack readManifest(ZipArchiveReader zip, Path file, Charset encoding) t return new Modpack(manifest.name(), manifest.author(), manifest.version(), manifest.minecraft().gameVersion(), description, encoding, manifest) { @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) { - return new CurseInstallTask(dependencyManager, zipFile, this, manifest, name, iconUrl); + public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) { + return new CurseInstallTask(dependencyManager, zipFile, this, manifest, instanceId, iconUrl); } }; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackCompletionTask.java index 730acc2106b..b97bf82bc83 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackCompletionTask.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.addon.mod.ModManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.ModpackConfiguration; import org.jackhuang.hmcl.modpack.ModpackCompletionException; import org.jackhuang.hmcl.modpack.curse.CurseMetaMod; @@ -53,7 +54,7 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask { private final DefaultDependencyManager dependency; private final DefaultGameRepository repository; private final ModManager modManager; - private final String version; + private final GameInstanceID instanceId; private final Path configurationFile; private ModpackConfiguration configuration; private McbbsModpackManifest manifest; @@ -63,16 +64,16 @@ public class McbbsModpackCompletionTask extends CompletableFutureTask { private final AtomicInteger finished = new AtomicInteger(0); private final AtomicBoolean notFound = new AtomicBoolean(false); - public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, String version) { - this(dependencyManager, version, null); + public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + this(dependencyManager, instanceId, null); } - public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, String version, ModpackConfiguration configuration) { + public McbbsModpackCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, ModpackConfiguration configuration) { this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); - this.modManager = repository.getModManager(version); - this.version = version; - this.configurationFile = repository.getModpackConfiguration(version); + this.modManager = repository.getModManager(instanceId); + this.instanceId = instanceId; + this.configurationFile = repository.getModpackConfiguration(instanceId); this.configuration = configuration; setStage("hmcl.modpack.download"); @@ -109,7 +110,7 @@ public CompletableFuture getFuture(TaskCompletableFuture executor) { throw new IOException("Unable to parse server manifest.json from " + manifest.getFileApi(), e); } - Path rootPath = repository.getVersionRoot(version); + Path rootPath = repository.getInstanceRoot(instanceId); Files.createDirectories(rootPath); Map localFiles = manifest.getFiles().stream().collect(Collectors.toMap(Function.identity(), Function.identity())); @@ -171,7 +172,7 @@ public CompletableFuture getFuture(TaskCompletableFuture executor) { manifest = remoteManifest.setFiles(newFiles); return executor.all(tasks.stream().filter(Objects::nonNull).collect(Collectors.toList())); })).thenAcceptAsync(wrapConsumer(unused1 -> { - Path manifestFile = repository.getModpackConfiguration(version); + Path manifestFile = repository.getModpackConfiguration(instanceId); JsonUtils.writeToJsonFile(manifestFile, new ModpackConfiguration<>(manifest, this.configuration.getType(), this.manifest.getName(), this.manifest.getVersion(), this.manifest.getFiles().stream() diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackExportTask.java index 1d20256ffcc..bdbc6832629 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackExportTask.java @@ -19,6 +19,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.modpack.ModAdviser; import org.jackhuang.hmcl.modpack.Modpack; @@ -45,13 +46,13 @@ public class McbbsModpackExportTask extends Task { private final DefaultGameRepository repository; - private final String version; + private final GameInstanceID instanceId; private final ModpackExportInfo info; private final Path modpackFile; - public McbbsModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo info, Path modpackFile) { + public McbbsModpackExportTask(DefaultGameRepository repository, GameInstanceID instanceId, ModpackExportInfo info, Path modpackFile) { this.repository = repository; - this.version = version; + this.instanceId = instanceId; this.info = info.validate(); this.modpackFile = modpackFile; @@ -69,11 +70,11 @@ public McbbsModpackExportTask(DefaultGameRepository repository, String version, @Override public void execute() throws Exception { ArrayList blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST); - blackList.add(version + ".jar"); - blackList.add(version + ".json"); + blackList.add(instanceId + ".jar"); + blackList.add(instanceId + ".json"); LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker"); try (var zip = new Zipper(modpackFile)) { - Path runDirectory = repository.getRunDirectory(version); + Path runDirectory = repository.getRunDirectory(instanceId); List files = new ArrayList<>(); zip.putDirectory(runDirectory, "overrides", path -> { if (Modpack.acceptFile(path, blackList, info.getWhitelist())) { @@ -88,9 +89,9 @@ public void execute() throws Exception { } }); - String gameVersion = repository.getGameVersion(version) - .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); + String gameVersion = repository.getGameVersion(instanceId) + .orElseThrow(() -> new IOException("Cannot parse the version of " + instanceId)); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), gameVersion); // Mcbbs manifest List addons = new ArrayList<>(); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java index 9666b4c0b89..110a1a82d37 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackLocalInstallTask.java @@ -21,7 +21,9 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.GameInstanceID; +import org.jackhuang.hmcl.game.GameInstanceManifest; +import org.jackhuang.hmcl.game.GameInstancePatch; import org.jackhuang.hmcl.modpack.MinecraftInstanceTask; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackConfiguration; @@ -43,29 +45,29 @@ public final class McbbsModpackLocalInstallTask extends Task { private final Path zipFile; private final Modpack modpack; private final McbbsModpackManifest manifest; - private final String name; + private final GameInstanceID instanceId; private final boolean update; private final DefaultGameRepository repository; private final MinecraftInstanceTask instanceTask; private final List> dependencies = new ArrayList<>(2); private final List> dependents = new ArrayList<>(4); - public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, McbbsModpackManifest manifest, String name) { + public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, McbbsModpackManifest manifest, GameInstanceID instanceId) { this.dependencyManager = dependencyManager; this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; - this.name = name; + this.instanceId = instanceId; this.repository = dependencyManager.getGameRepository(); - Path run = repository.getRunDirectory(name); + Path run = repository.getRunDirectory(instanceId); - Path json = repository.getModpackConfiguration(name); - if (repository.hasVersion(name) && Files.notExists(json)) - throw new IllegalArgumentException("Version " + name + " already exists."); - this.update = repository.hasVersion(name); + Path json = repository.getModpackConfiguration(instanceId); + if (repository.hasInstance(instanceId) && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " already exists."); + this.update = repository.hasInstance(instanceId); - GameBuilder builder = dependencyManager.gameBuilder().name(name); + GameBuilder builder = dependencyManager.newGameBuilder().name(instanceId); for (McbbsModpackManifest.Addon addon : manifest.getAddons()) { builder.version(addon.getId(), addon.getVersion()); } @@ -73,7 +75,7 @@ public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, dependents.add(builder.buildAsync()); onDone().register(event -> { if (event.isFailed()) - repository.removeVersionFromDisk(name); + repository.removeInstanceFromDisk(instanceId); }); ModpackConfiguration config = null; @@ -82,12 +84,12 @@ public McbbsModpackLocalInstallTask(DefaultDependencyManager dependencyManager, config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(McbbsModpackManifest.class)); if (!McbbsModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Version " + name + " is not a Mcbbs modpack. Cannot update this version."); + throw new IllegalArgumentException("Instance " + instanceId + " is not a Mcbbs modpack. Cannot update this instance."); } } catch (JsonParseException | IOException ignore) { } dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack")); - instanceTask = new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, McbbsModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)); + instanceTask = new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, McbbsModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(instanceId)); dependents.add(instanceTask.withStage("hmcl.modpack")); } @@ -103,21 +105,21 @@ public List> getDependencies() { @Override public void execute() throws Exception { - Version version = repository.readVersionJson(name); - Optional mcbbsPatch = version.getPatches().stream().filter(patch -> PATCH_NAME.equals(patch.getId())).findFirst(); + GameInstanceManifest instanceManifest = repository.getInstanceManifest(instanceId); + Optional mcbbsPatch = instanceManifest.getPatches().stream().filter(patch -> PATCH_NAME.equals(patch.id())).findFirst(); if (!update) { - Version patch = new Version(PATCH_NAME).setLibraries(manifest.getLibraries()); - dependencies.add(repository.saveAsync(version.addPatch(patch))); + GameInstancePatch patch = new GameInstancePatch(PATCH_NAME).withLibraries(manifest.getLibraries()); + dependencies.add(repository.saveAsync(instanceManifest.addPatch(patch))); } else if (mcbbsPatch.isPresent()) { // This mcbbs modpack was installed by HMCL. - Version patch = mcbbsPatch.get().setLibraries(manifest.getLibraries()); - dependencies.add(repository.saveAsync(version.addPatch(patch))); + GameInstancePatch patch = mcbbsPatch.get().withLibraries(manifest.getLibraries()); + dependencies.add(repository.saveAsync(instanceManifest.addPatch(patch))); } else { // This mcbbs modpack was installed by other launchers. // TODO: maintain libraries. } - dependencies.add(new McbbsModpackCompletionTask(dependencyManager, name, instanceTask.getResult())); + dependencies.add(new McbbsModpackCompletionTask(dependencyManager, instanceId, instanceTask.getResult())); } private static final String PATCH_NAME = "mcbbs"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackManifest.java index 1213aacf5ce..69e63a1ee59 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackManifest.java @@ -20,6 +20,7 @@ import com.google.gson.JsonParseException; import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.LaunchOptions; import org.jackhuang.hmcl.game.Library; import org.jackhuang.hmcl.modpack.Modpack; @@ -424,8 +425,8 @@ public Modpack toModpack(Charset encoding) throws IOException { .orElseThrow(() -> new IOException("Cannot find game version")).getVersion(); return new Modpack(name, author, version, gameVersion, description, encoding, this) { @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) { - return new McbbsModpackLocalInstallTask(dependencyManager, zipFile, this, McbbsModpackManifest.this, name); + public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) { + return new McbbsModpackLocalInstallTask(dependencyManager, zipFile, this, McbbsModpackManifest.this, instanceId); } }; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackProvider.java index 966c08bfe15..5c63d3d0649 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackProvider.java @@ -21,6 +21,7 @@ import kala.compress.archivers.zip.ZipArchiveEntry; import kala.compress.archivers.zip.ZipArchiveReader; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.game.LaunchOptions; import org.jackhuang.hmcl.modpack.*; import org.jackhuang.hmcl.task.Task; @@ -40,16 +41,16 @@ public String getName() { } @Override - public Task createCompletionTask(DefaultDependencyManager dependencyManager, String version) { - return new McbbsModpackCompletionTask(dependencyManager, version); + public Task createCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + return new McbbsModpackCompletionTask(dependencyManager, instanceId); } @Override - public Task createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { + public Task createUpdateTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { if (!(modpack.getManifest() instanceof McbbsModpackManifest mcbbsModpackManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new McbbsModpackLocalInstallTask(dependencyManager, zipFile, modpack, mcbbsModpackManifest, name)); + return new ModpackUpdateTask(dependencyManager.getGameRepository(), instanceId, new McbbsModpackLocalInstallTask(dependencyManager, zipFile, modpack, mcbbsModpackManifest, instanceId)); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackRemoteInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackRemoteInstallTask.java index bfd6ec23d9e..c8e0b80c05f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackRemoteInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/mcbbs/McbbsModpackRemoteInstallTask.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.ModpackConfiguration; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -34,24 +35,24 @@ public class McbbsModpackRemoteInstallTask extends Task { - private final String name; + private final GameInstanceID instanceId; private final DefaultDependencyManager dependency; private final DefaultGameRepository repository; private final List> dependencies = new ArrayList<>(1); private final List> dependents = new ArrayList<>(1); private final McbbsModpackManifest manifest; - public McbbsModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, McbbsModpackManifest manifest, String name) { - this.name = name; + public McbbsModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, McbbsModpackManifest manifest, GameInstanceID instanceId) { + this.instanceId = instanceId; this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); this.manifest = manifest; - Path json = repository.getModpackConfiguration(name); - if (repository.hasVersion(name) && Files.notExists(json)) - throw new IllegalArgumentException("Version " + name + " already exists."); + Path json = repository.getModpackConfiguration(instanceId); + if (repository.hasInstance(instanceId) && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.gameBuilder().name(name); + GameBuilder builder = dependencyManager.newGameBuilder().name(instanceId); for (McbbsModpackManifest.Addon addon : manifest.getAddons()) { builder.version(addon.getId(), addon.getVersion()); } @@ -59,7 +60,7 @@ public McbbsModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, dependents.add(builder.buildAsync()); onDone().register(event -> { if (event.isFailed()) - repository.removeVersionFromDisk(name); + repository.removeInstanceFromDisk(instanceId); }); ModpackConfiguration config; @@ -68,7 +69,7 @@ public McbbsModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(McbbsModpackManifest.class)); if (!MODPACK_TYPE.equals(config.getType())) - throw new IllegalArgumentException("Version " + name + " is not a Mcbbs modpack. Cannot update this version."); + throw new IllegalArgumentException("Instance " + instanceId + " is not a Mcbbs modpack. Cannot update this instance."); } } catch (JsonParseException | IOException ignore) { } @@ -86,7 +87,7 @@ public List> getDependencies() { @Override public void execute() throws Exception { - dependencies.add(new McbbsModpackCompletionTask(dependency, name, new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList()))); + dependencies.add(new McbbsModpackCompletionTask(dependency, instanceId, new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList()))); } public static final String MODPACK_TYPE = "Server"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthCompletionTask.java index 6794a52cf3b..d44673ac8be 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthCompletionTask.java @@ -20,6 +20,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.addon.mod.ModManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.ModpackCompletionException; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.Task; @@ -43,7 +44,7 @@ public class ModrinthCompletionTask extends Task { private final DefaultDependencyManager dependency; private final DefaultGameRepository repository; private final ModManager modManager; - private final String version; + private final GameInstanceID instanceId; private ModrinthManifest manifest; private final List> dependencies = new ArrayList<>(); @@ -55,29 +56,29 @@ public class ModrinthCompletionTask extends Task { * Constructor. * * @param dependencyManager the dependency manager. - * @param version the existent and physical version. + * @param instanceId the existent and physical version. */ - public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version) { - this(dependencyManager, version, null); + public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + this(dependencyManager, instanceId, null); } /** * Constructor. * * @param dependencyManager the dependency manager. - * @param version the existent and physical version. + * @param instanceId the existent and physical version. * @param manifest the CurseForgeModpack manifest. */ - public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, String version, ModrinthManifest manifest) { + public ModrinthCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, ModrinthManifest manifest) { this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); - this.modManager = repository.getModManager(version); - this.version = version; + this.modManager = repository.getModManager(instanceId); + this.instanceId = instanceId; this.manifest = manifest; if (manifest == null) try { - Path manifestFile = repository.getVersionRoot(version).resolve("modrinth.index.json"); + Path manifestFile = repository.getInstanceRoot(instanceId).resolve("modrinth.index.json"); if (Files.exists(manifestFile)) this.manifest = JsonUtils.fromJsonFile(manifestFile, ModrinthManifest.class); } catch (Exception e) { @@ -102,7 +103,7 @@ public void execute() throws Exception { if (manifest == null) return; - Path runDirectory = FileUtils.toAbsolute(repository.getRunDirectory(version)); + Path runDirectory = FileUtils.toAbsolute(repository.getRunDirectory(instanceId)); Path modsDirectory = runDirectory.resolve("mods"); for (ModrinthManifest.File file : manifest.getFiles()) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java index 00e9a0cc560..886dd9ec8df 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthInstallTask.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.*; import org.jackhuang.hmcl.task.CacheFileTask; import org.jackhuang.hmcl.task.Task; @@ -44,7 +45,7 @@ public class ModrinthInstallTask extends Task { private final Path zipFile; private final Modpack modpack; private final ModrinthManifest manifest; - private final String name; + private final GameInstanceID instanceId; private final String iconUrl; private final Path run; private final ModpackConfiguration config; @@ -53,21 +54,21 @@ public class ModrinthInstallTask extends Task { private final List> dependents = new ArrayList<>(4); private final List> dependencies = new ArrayList<>(1); - public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, ModrinthManifest manifest, String name, String iconUrl) { + public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, ModrinthManifest manifest, GameInstanceID instanceId, String iconUrl) { this.dependencyManager = dependencyManager; this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; - this.name = name; + this.instanceId = instanceId; this.iconUrl = iconUrl; this.repository = dependencyManager.getGameRepository(); - this.run = repository.getRunDirectory(name); + this.run = repository.getRunDirectory(instanceId); - Path json = repository.getModpackConfiguration(name); - if (repository.hasVersion(name) && Files.notExists(json)) - throw new IllegalArgumentException("Version " + name + " already exists."); + Path json = repository.getModpackConfiguration(instanceId); + if (repository.hasInstance(instanceId) && Files.notExists(json)) + throw new IllegalArgumentException("Version " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.gameBuilder().name(name).gameVersion(manifest.getGameVersion()); + GameBuilder builder = dependencyManager.newGameBuilder().name(instanceId).gameVersion(manifest.getGameVersion()); for (Map.Entry modLoader : manifest.getDependencies().entrySet()) { switch (modLoader.getKey()) { case "minecraft": @@ -96,7 +97,7 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF Exception ex = event.getTask().getException(); if (event.isFailed()) { if (!(ex instanceof ModpackCompletionException)) { - repository.removeVersionFromDisk(name); + repository.removeInstanceFromDisk(instanceId); } } }); @@ -107,7 +108,7 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ModrinthManifest.class)); if (!ModrinthModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Version " + name + " is not a Modrinth modpack. Cannot update this version."); + throw new IllegalArgumentException("Version " + instanceId + " is not a Modrinth modpack. Cannot update this version."); } } catch (JsonParseException | IOException ignore) { } @@ -115,7 +116,7 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF this.config = config; List subDirectories = Arrays.asList("/client-overrides", "/overrides"); dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), subDirectories, any -> true, config).withStage("hmcl.modpack")); - dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), subDirectories, manifest, ModrinthModpackProvider.INSTANCE, manifest.getName(), manifest.getVersionId(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack")); + dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), subDirectories, manifest, ModrinthModpackProvider.INSTANCE, manifest.getName(), manifest.getVersionId(), repository.getModpackConfiguration(instanceId)).withStage("hmcl.modpack")); URI iconUri = NetworkUtils.toURIOrNull(iconUrl); if (iconUri != null) { @@ -126,7 +127,7 @@ public ModrinthInstallTask(DefaultDependencyManager dependencyManager, Path zipF dependents.add(downloadIconTask = new CacheFileTask(dependencyManager.getDownloadProvider().injectURLWithCandidates(iconUrl))); } } - dependencies.add(new ModrinthCompletionTask(dependencyManager, name, manifest)); + dependencies.add(new ModrinthCompletionTask(dependencyManager, instanceId, manifest)); } @Override @@ -152,7 +153,7 @@ public void execute() throws Exception { } } - Path root = repository.getVersionRoot(name); + Path root = repository.getInstanceRoot(instanceId); Files.createDirectories(root); JsonUtils.writeToJsonFile(root.resolve("modrinth.index.json"), manifest); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackExportTask.java index c589efce48b..25edbf60a0f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackExportTask.java @@ -27,6 +27,7 @@ import org.jackhuang.hmcl.addon.repository.ModrinthRemoteAddonRepository; import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.ModAdviser; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackExportInfo; @@ -43,13 +44,13 @@ public class ModrinthModpackExportTask extends Task { private final DefaultGameRepository repository; - private final String version; + private final GameInstanceID instanceId; private final ModpackExportInfo info; private final Path modpackFile; - public ModrinthModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo info, Path modpackFile) { + public ModrinthModpackExportTask(DefaultGameRepository repository, GameInstanceID instanceId, ModpackExportInfo info, Path modpackFile) { this.repository = repository; - this.version = version; + this.instanceId = instanceId; this.info = info.validate(); this.modpackFile = modpackFile; @@ -69,9 +70,9 @@ private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath) t return null; } - boolean isDisabled = repository.getModManager(version).isDisabled(file); + boolean isDisabled = repository.getModManager(instanceId).isDisabled(file); if (isDisabled) { - relativePath = repository.getModManager(version).enableMod(Paths.get(relativePath)).toString(); + relativePath = repository.getModManager(instanceId).enableMod(Paths.get(relativePath)).toString(); } LocalModFile localModFile = null; @@ -128,11 +129,11 @@ private ModrinthManifest.File tryGetRemoteFile(Path file, String relativePath) t @Override public void execute() throws Exception { ArrayList blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST); - blackList.add(version + ".jar"); - blackList.add(version + ".json"); + blackList.add(instanceId + ".jar"); + blackList.add(instanceId + ".json"); LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker"); try (var zip = new Zipper(modpackFile)) { - Path runDirectory = repository.getRunDirectory(version); + Path runDirectory = repository.getRunDirectory(instanceId); List files = new ArrayList<>(); Set filesInManifest = new HashSet<>(); @@ -170,9 +171,9 @@ public void execute() throws Exception { return Modpack.acceptFile(path, blackList, info.getWhitelist()); }); - String gameVersion = repository.getGameVersion(version) - .orElseThrow(() -> new IOException("Cannot parse the version of " + version)); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(version), gameVersion); + String gameVersion = repository.getGameVersion(instanceId) + .orElseThrow(() -> new IOException("Cannot parse the version of " + instanceId)); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), gameVersion); Map dependencies = new HashMap<>(); dependencies.put("minecraft", gameVersion); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackProvider.java index fdaa506c920..6b4f7374bf5 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/modrinth/ModrinthModpackProvider.java @@ -20,6 +20,7 @@ import com.google.gson.JsonParseException; import kala.compress.archivers.zip.ZipArchiveReader; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackProvider; @@ -41,16 +42,16 @@ public String getName() { } @Override - public Task createCompletionTask(DefaultDependencyManager dependencyManager, String version) { - return new ModrinthCompletionTask(dependencyManager, version); + public Task createCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + return new ModrinthCompletionTask(dependencyManager, instanceId); } @Override - public Task createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { + public Task createUpdateTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { if (!(modpack.getManifest() instanceof ModrinthManifest modrinthManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new ModrinthInstallTask(dependencyManager, zipFile, modpack, modrinthManifest, name, null)); + return new ModpackUpdateTask(dependencyManager.getGameRepository(), instanceId, new ModrinthInstallTask(dependencyManager, zipFile, modpack, modrinthManifest, instanceId, null)); } @Override @@ -58,8 +59,8 @@ public Modpack readManifest(ZipArchiveReader zip, Path file, Charset encoding) t ModrinthManifest manifest = JsonUtils.fromNonNullJson(CompressingUtils.readTextZipEntry(zip, "modrinth.index.json"), ModrinthManifest.class); return new Modpack(manifest.getName(), "", manifest.getVersionId(), manifest.getGameVersion(), manifest.getSummary(), encoding, manifest) { @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) { - return new ModrinthInstallTask(dependencyManager, zipFile, this, manifest, name, iconUrl); + public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) { + return new ModrinthInstallTask(dependencyManager, zipFile, this, manifest, instanceId, iconUrl); } }; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCInstancePatch.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCInstancePatch.java index 452faace338..7391621fc7f 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCInstancePatch.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCInstancePatch.java @@ -24,7 +24,6 @@ import org.jackhuang.hmcl.util.Immutable; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; -import org.jackhuang.hmcl.util.gson.JsonMap; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.io.NetworkUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; @@ -200,7 +199,7 @@ public static MultiMCInstancePatch read(String componentID, String text) { } public static final class ResolvedInstance { - private final Version version; + private final GameInstanceManifest manifest; private final String gameVersion; @@ -209,16 +208,16 @@ public static final class ResolvedInstance { private final List jarModFileNames; private final List mavenOnlyFiles; - public ResolvedInstance(Version version, String gameVersion, Library mainJar, List jarModFileNames, List mavenOnlyFiles) { - this.version = version; + public ResolvedInstance(GameInstanceManifest manifest, String gameVersion, Library mainJar, List jarModFileNames, List mavenOnlyFiles) { + this.manifest = manifest; this.gameVersion = gameVersion; this.mainJar = mainJar; this.jarModFileNames = jarModFileNames; this.mavenOnlyFiles = mavenOnlyFiles; } - public Version getVersion() { - return version; + public GameInstanceManifest getManifest() { + return manifest; } public String getGameVersion() { @@ -245,10 +244,10 @@ public List getMavenOnlyFiles() { * See to do marks below for more information

* * @param patches List of all Json-Patch. - * @param versionID the version ID. Used when constructing a Version. + * @param instanceId the version ID. Used when constructing a manifest. * @return The resolved instance. */ - public static ResolvedInstance resolveArtifact(List patches, String versionID) { + public static ResolvedInstance resolveArtifact(List patches, GameInstanceID instanceId) { if (patches.isEmpty()) { throw new IllegalArgumentException("Empty components."); } @@ -386,12 +385,12 @@ public static ResolvedInstance resolveArtifact(List patche mainClass = "org.jackhuang.hmcl.HMCLMultiMCBootstrap"; } - Version version = new Version(versionID) - .setArguments(new Arguments().addGameArguments(minecraftArguments).addJVMArgumentsDirect(jvmArguments)) - .setMainClass(mainClass) - .setLibraries(libraries) - .setAssetIndex(assetIndex) - .setDownload(new JsonMap<>(Collections.singletonMap(DownloadType.CLIENT, mainJar.getRawDownloadInfo()))); + GameInstanceManifest version = new GameInstanceManifest(instanceId) + .withArguments(new Arguments().addGameArguments(minecraftArguments).addJVMArgumentsDirect(jvmArguments)) + .withMainClass(mainClass) + .withLibraries(libraries) + .withAssetIndex(assetIndex) + .withDownloads(Collections.singletonMap(DownloadType.CLIENT, mainJar.getRawDownloadInfo())); /* TODO: Official Version-Json can only store one pre-defined GameJavaVersion, including 8, 11, 16, 17 and 21. An array of all suitable java versions are NOT supported. @@ -404,7 +403,7 @@ public static ResolvedInstance resolveArtifact(List patche for (int i = javaMajors.length - 1; i >= 0; i--) { GameJavaVersion jv = GameJavaVersion.get(javaMajors[i]); if (jv != null) { - version = version.setJavaVersion(jv); + version = version.withJavaVersion(jv); break javaMajors; } } @@ -412,8 +411,6 @@ public static ResolvedInstance resolveArtifact(List patche message.append(" - Java Version Range: ").append(Arrays.toString(javaMajors)).append('\n'); } - version = version.markAsResolved(); - String gameVersion = null; for (MultiMCInstancePatch patch : patches) { if (MultiMCComponents.getComponent(patch.getID()) == LibraryAnalyzer.LibraryType.MINECRAFT) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackExportTask.java index ca0daeebd76..a27f9ca961a 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackExportTask.java @@ -19,6 +19,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.ModAdviser; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackExportInfo; @@ -42,18 +43,18 @@ */ public class MultiMCModpackExportTask extends Task { private final DefaultGameRepository repository; - private final String versionId; + private final GameInstanceID instanceId; private final List whitelist; private final MultiMCInstanceConfiguration configuration; private final Path output; /** * @param output mod pack file. - * @param versionId to locate version.json + * @param instanceId to locate version.json */ - public MultiMCModpackExportTask(DefaultGameRepository repository, String versionId, List whitelist, MultiMCInstanceConfiguration configuration, Path output) { + public MultiMCModpackExportTask(DefaultGameRepository repository, GameInstanceID instanceId, List whitelist, MultiMCInstanceConfiguration configuration, Path output) { this.repository = repository; - this.versionId = versionId; + this.instanceId = instanceId; this.whitelist = whitelist; this.configuration = configuration; this.output = output; @@ -72,15 +73,15 @@ public MultiMCModpackExportTask(DefaultGameRepository repository, String version @Override public void execute() throws Exception { ArrayList blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST); - blackList.add(versionId + ".jar"); - blackList.add(versionId + ".json"); + blackList.add(instanceId + ".jar"); + blackList.add(instanceId + ".json"); LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker"); try (Zipper zip = new Zipper(output)) { - zip.putDirectory(repository.getRunDirectory(versionId), ".minecraft", path -> Modpack.acceptFile(path, blackList, whitelist)); + zip.putDirectory(repository.getRunDirectory(instanceId), ".minecraft", path -> Modpack.acceptFile(path, blackList, whitelist)); - String gameVersion = repository.getGameVersion(versionId) - .orElseThrow(() -> new IOException("Cannot parse the version of " + versionId)); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(versionId), gameVersion); + String gameVersion = repository.getGameVersion(instanceId) + .orElseThrow(() -> new IOException("Cannot parse the version of " + instanceId)); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), gameVersion); List components = new ArrayList<>(); components.add(new MultiMCManifest.MultiMCManifestComponent(true, false, MultiMCComponents.getComponent(MINECRAFT), gameVersion)); diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java index a028e6200d8..08ea5babbe8 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackInstallTask.java @@ -24,10 +24,7 @@ import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; import org.jackhuang.hmcl.download.game.GameDownloadTask; import org.jackhuang.hmcl.download.game.GameLibrariesTask; -import org.jackhuang.hmcl.game.Artifact; -import org.jackhuang.hmcl.game.DefaultGameRepository; -import org.jackhuang.hmcl.game.Library; -import org.jackhuang.hmcl.game.Version; +import org.jackhuang.hmcl.game.*; import org.jackhuang.hmcl.modpack.MinecraftInstanceTask; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackConfiguration; @@ -79,27 +76,27 @@ public final class MultiMCModpackInstallTask extends Task> dependents = new ArrayList<>(); private final List> dependencies = new ArrayList<>(); private final DefaultDependencyManager dependencyManager; - public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, MultiMCInstanceConfiguration manifest, String name) { + public MultiMCModpackInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, MultiMCInstanceConfiguration manifest, GameInstanceID instanceId) { this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; - this.name = name; + this.instanceId = instanceId; this.dependencyManager = dependencyManager; this.repository = dependencyManager.getGameRepository(); - Path json = repository.getModpackConfiguration(name); - if (repository.hasVersion(name) && Files.notExists(json)) - throw new IllegalArgumentException("Version " + name + " already exists."); + Path json = repository.getModpackConfiguration(instanceId); + if (repository.hasInstance(instanceId) && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " already exists."); onDone().register(event -> { if (event.isFailed()) - repository.removeVersionFromDisk(name); + repository.removeInstanceFromDisk(instanceId); }); } @@ -112,8 +109,8 @@ public boolean doPreExecute() { public void preExecute() throws Exception { // Stage #0: General Setup { - Path run = repository.getRunDirectory(name); - Path json = repository.getModpackConfiguration(name); + Path run = repository.getRunDirectory(instanceId); + Path json = repository.getModpackConfiguration(instanceId); ModpackConfiguration config = null; try { @@ -121,7 +118,7 @@ public void preExecute() throws Exception { config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(MultiMCInstanceConfiguration.class)); if (!MultiMCModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Version " + name + " is not a MultiMC modpack. Cannot update this version."); + throw new IllegalArgumentException("Instance " + instanceId + " is not a MultiMC modpack. Cannot update this instance."); } } catch (JsonParseException | IOException ignore) { } @@ -133,7 +130,7 @@ public void preExecute() throws Exception { // TODO: Optimize unbearably slow ModpackInstallTask dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList(mcDirectory), any -> true, config).withStage("hmcl.modpack")); - dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(mcDirectory), manifest, MultiMCModpackProvider.INSTANCE, manifest.getName(), null, repository.getModpackConfiguration(name)).withStage("hmcl.modpack")); + dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList(mcDirectory), manifest, MultiMCModpackProvider.INSTANCE, manifest.getName(), null, repository.getModpackConfiguration(instanceId)).withStage("hmcl.modpack")); } // Stage #1: Load all related Json-Patch from meta maven or local mod pack. @@ -240,7 +237,7 @@ public void execute() throws Exception { for (int i = dependents.size() - 1; i >= 0; i--) { Task task = dependents.get(i); if (task instanceof MMCInstancePatchesAssembleTask) { - artifact = MultiMCInstancePatch.resolveArtifact(((MMCInstancePatchesAssembleTask) task).getResult(), name); + artifact = MultiMCInstancePatch.resolveArtifact(((MMCInstancePatchesAssembleTask) task).getResult(), instanceId); break; } } @@ -252,23 +249,23 @@ public void execute() throws Exception { Path libraries = root.resolve("libraries"); if (Files.exists(libraries)) - FileUtils.copyDirectory(libraries, repository.getVersionRoot(name).resolve("libraries")); + FileUtils.copyDirectory(libraries, repository.getInstanceRoot(instanceId).resolve("libraries")); - for (Library library : artifact.getVersion().getLibraries()) { + for (Library library : artifact.getManifest().getLibraries()) { if ("local".equals(library.getHint())) { /* TODO: Determine whether we should erase community fields, like 'hint' and 'filename' from version json. Retain them will facilitate compatibility, as some embedded libraries may check where their JAR is. Meanwhile, potential compatibility issue with other launcher which never supports these fields might occur. Here, we make the file stored twice, to keep maximum compatibility. */ - Path from = repository.getLibraryFile(artifact.getVersion(), library); - Path target = repository.getLibraryFile(artifact.getVersion(), library.withoutCommunityFields()); + Path from = repository.getLibraryFile(artifact.getManifest(), library); + Path target = repository.getLibraryFile(artifact.getManifest(), library.withoutCommunityFields()); Files.createDirectories(target.getParent()); Files.copy(from, target, StandardCopyOption.REPLACE_EXISTING); } } try (InputStream input = MaintainTask.class.getResourceAsStream("/assets/game/HMCLMultiMCBootstrap-1.0.jar")) { - Path libraryPath = repository.getLibraryFile(artifact.getVersion(), MultiMCInstancePatch.BOOTSTRAP_LIBRARY); + Path libraryPath = repository.getLibraryFile(artifact.getManifest(), MultiMCInstancePatch.BOOTSTRAP_LIBRARY); Files.createDirectories(libraryPath.getParent()); Files.copy(Objects.requireNonNull(input, "Bundled HMCLMultiMCBootstrap is missing."), libraryPath, StandardCopyOption.REPLACE_EXISTING); @@ -278,21 +275,21 @@ public void execute() throws Exception { if (iconKey != null) { Path iconFile = root.resolve(iconKey + ".png"); if (Files.exists(iconFile)) { - FileUtils.copyFile(iconFile, repository.getVersionRoot(name).resolve("icon.png")); + FileUtils.copyFile(iconFile, repository.getInstanceRoot(instanceId).resolve("icon.png")); } } } // Stage #5: Assemble game files. { - Version version = artifact.getVersion(); + GameInstanceManifest instanceManifest = artifact.getManifest(); - dependencies.add(repository.saveAsync(artifact.getVersion())); - dependencies.add(new GameAssetDownloadTask(dependencyManager, version, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)); + dependencies.add(repository.saveAsync(artifact.getManifest())); + dependencies.add(new GameAssetDownloadTask(dependencyManager, instanceManifest, GameAssetDownloadTask.DOWNLOAD_INDEX_FORCIBLY, true)); dependencies.add(new GameLibrariesTask( dependencyManager, // TODO: check integrity of maven-only files when launching games? - version.setLibraries(Lang.merge(version.getLibraries(), artifact.getMavenOnlyFiles())), + instanceManifest.withLibraries(Lang.merge(instanceManifest.getLibraries(), artifact.getMavenOnlyFiles())), true )); @@ -304,9 +301,9 @@ public void execute() throws Exception { Objects.equals(gameVersion, mainJarArtifact.getVersion()) && "client".equals(mainJarArtifact.getClassifier()) ) { - dependencies.add(new GameDownloadTask(dependencyManager, gameVersion, version)); + dependencies.add(new GameDownloadTask(dependencyManager, gameVersion, instanceManifest)); } else { - dependencies.add(new GameDownloadTask(dependencyManager, null, version)); + dependencies.add(new GameDownloadTask(dependencyManager, null, instanceManifest)); } } @@ -338,7 +335,7 @@ public void postExecute() throws Exception { Path root = getRootPath(fs).resolve("jarmods"); try (FileSystem mc = CompressingUtils.writable( - repository.getVersionRoot(name).resolve(name + ".jar") + repository.getInstanceRoot(instanceId).resolve(instanceId + ".jar") ).setAutoDetectEncoding(true).build()) { for (String fileName : files) { try (FileSystem jm = CompressingUtils.readonly(root.resolve(fileName)).setAutoDetectEncoding(true).build()) { diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackProvider.java index 3aa4fa1e592..479095d981d 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/multimc/MultiMCModpackProvider.java @@ -20,6 +20,7 @@ import kala.compress.archivers.zip.ZipArchiveEntry; import kala.compress.archivers.zip.ZipArchiveReader; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackProvider; @@ -41,16 +42,16 @@ public String getName() { } @Override - public Task createCompletionTask(DefaultDependencyManager dependencyManager, String version) { + public Task createCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { return null; } @Override - public Task createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { + public Task createUpdateTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { if (!(modpack.getManifest() instanceof MultiMCInstanceConfiguration multiMCInstanceConfiguration)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new MultiMCModpackInstallTask(dependencyManager, zipFile, modpack, multiMCInstanceConfiguration, name)); + return new ModpackUpdateTask(dependencyManager.getGameRepository(), instanceId, new MultiMCModpackInstallTask(dependencyManager, zipFile, modpack, multiMCInstanceConfiguration, instanceId)); } private static String getRootEntryName(ZipArchiveReader file) throws IOException { @@ -85,8 +86,8 @@ public Modpack readManifest(ZipArchiveReader modpackFile, Path modpackPath, Char MultiMCInstanceConfiguration cfg = new MultiMCInstanceConfiguration(name, instanceStream, manifest); return new Modpack(cfg.getName(), "", "", cfg.getGameVersion(), cfg.getNotes(), encoding, cfg) { @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) { - return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, name); + public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) { + return new MultiMCModpackInstallTask(dependencyManager, zipFile, this, cfg, instanceId); } }; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java index ab06e48e380..2caca9ab70e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackCompletionTask.java @@ -22,6 +22,7 @@ import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.game.DefaultGameRepository; import org.jackhuang.hmcl.addon.LocalAddonManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.ModpackConfiguration; import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.GetTask; @@ -43,24 +44,24 @@ public class ServerModpackCompletionTask extends Task { private final DefaultDependencyManager dependencyManager; private final DefaultGameRepository repository; - private final String version; + private final GameInstanceID instanceId; private ModpackConfiguration manifest; private GetTask dependent; private ServerModpackManifest remoteManifest; private final List> dependencies = new ArrayList<>(); - public ServerModpackCompletionTask(DefaultDependencyManager dependencyManager, String version) { - this(dependencyManager, version, null); + public ServerModpackCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + this(dependencyManager, instanceId, null); } - public ServerModpackCompletionTask(DefaultDependencyManager dependencyManager, String version, ModpackConfiguration manifest) { + public ServerModpackCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, ModpackConfiguration manifest) { this.dependencyManager = dependencyManager; this.repository = dependencyManager.getGameRepository(); - this.version = version; + this.instanceId = instanceId; if (manifest == null) { try { - Path manifestFile = repository.getModpackConfiguration(version); + Path manifestFile = repository.getModpackConfiguration(instanceId); if (Files.exists(manifestFile)) { this.manifest = JsonUtils.fromJsonFile(manifestFile, ModpackConfiguration.typeOf(ServerModpackManifest.class)); } @@ -112,7 +113,7 @@ public void execute() throws Exception { Map oldAddons = toMap(manifest.getManifest().getAddons()); Map newAddons = toMap(remoteManifest.getAddons()); if (!Objects.equals(oldAddons, newAddons)) { - GameBuilder builder = dependencyManager.gameBuilder().name(version); + GameBuilder builder = dependencyManager.newGameBuilder().name(instanceId); for (ServerModpackManifest.Addon addon : remoteManifest.getAddons()) { builder.version(addon.getId(), addon.getVersion()); } @@ -120,7 +121,7 @@ public void execute() throws Exception { dependencies.add(builder.buildAsync()); } - Path rootPath = repository.getVersionRoot(version).toAbsolutePath().normalize(); + Path rootPath = repository.getInstanceRoot(instanceId).toAbsolutePath().normalize(); Map files = manifest.getManifest().getFiles().stream() .collect(Collectors.toMap(ModpackConfiguration.FileInformation::getPath, Function.identity())); @@ -128,7 +129,7 @@ public void execute() throws Exception { Set remoteFiles = remoteManifest.getFiles().stream().map(ModpackConfiguration.FileInformation::getPath) .collect(Collectors.toSet()); - Path runDirectory = repository.getRunDirectory(version).toAbsolutePath().normalize(); + Path runDirectory = repository.getRunDirectory(instanceId).toAbsolutePath().normalize(); Path modsDirectory = runDirectory.resolve("mods"); int total = 0; @@ -192,7 +193,7 @@ public boolean doPostExecute() { @Override public void postExecute() throws Exception { if (manifest == null || StringUtils.isBlank(manifest.getManifest().getFileApi())) return; - Path manifestFile = repository.getModpackConfiguration(version); + Path manifestFile = repository.getModpackConfiguration(instanceId); Files.createDirectories(manifestFile.getParent()); JsonUtils.writeToJsonFile(manifestFile, new ModpackConfiguration<>(remoteManifest, this.manifest.getType(), this.manifest.getName(), this.manifest.getVersion(), remoteManifest.getFiles())); } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackExportTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackExportTask.java index e943675e0f9..c2bcfcbe702 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackExportTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackExportTask.java @@ -19,6 +19,7 @@ import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.ModAdviser; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackConfiguration; @@ -41,13 +42,13 @@ public class ServerModpackExportTask extends Task { private final DefaultGameRepository repository; - private final String versionId; + private final GameInstanceID instanceId; private final ModpackExportInfo exportInfo; private final Path modpackFile; - public ServerModpackExportTask(DefaultGameRepository repository, String version, ModpackExportInfo exportInfo, Path modpackFile) { + public ServerModpackExportTask(DefaultGameRepository repository, GameInstanceID instanceId, ModpackExportInfo exportInfo, Path modpackFile) { this.repository = repository; - this.versionId = version; + this.instanceId = instanceId; this.exportInfo = exportInfo.validate(); this.modpackFile = modpackFile; @@ -65,11 +66,11 @@ public ServerModpackExportTask(DefaultGameRepository repository, String version, @Override public void execute() throws Exception { ArrayList blackList = new ArrayList<>(ModAdviser.MODPACK_BLACK_LIST); - blackList.add(versionId + ".jar"); - blackList.add(versionId + ".json"); + blackList.add(instanceId + ".jar"); + blackList.add(instanceId + ".json"); LOG.info("Compressing game files without some files in blacklist, including files or directories: usernamecache.json, asm, logs, backups, versions, assets, usercache.json, libraries, crash-reports, launcher_profiles.json, NVIDIA, TCNodeTracker"); try (Zipper zip = new Zipper(modpackFile)) { - Path runDirectory = repository.getRunDirectory(versionId); + Path runDirectory = repository.getRunDirectory(instanceId); List files = new ArrayList<>(); zip.putDirectory(runDirectory, "overrides", path -> { if (Modpack.acceptFile(path, blackList, exportInfo.getWhitelist())) { @@ -84,9 +85,9 @@ public void execute() throws Exception { } }); - String gameVersion = repository.getGameVersion(versionId) - .orElseThrow(() -> new IOException("Cannot parse the version of " + versionId)); - LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedPreservingPatchesVersion(versionId), gameVersion); + String gameVersion = repository.getGameVersion(instanceId) + .orElseThrow(() -> new IOException("Cannot parse the version of " + instanceId)); + LibraryAnalyzer analyzer = LibraryAnalyzer.analyze(repository.getResolvedInstanceManifest(instanceId), gameVersion); List addons = new ArrayList<>(); addons.add(new ServerModpackManifest.Addon(MINECRAFT.getPatchId(), gameVersion)); analyzer.getVersion(FORGE).ifPresent(forgeVersion -> diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java index 5a1d382a685..fc34ab1f6fe 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackLocalInstallTask.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.MinecraftInstanceTask; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackConfiguration; @@ -40,24 +41,24 @@ public class ServerModpackLocalInstallTask extends Task { private final Path zipFile; private final Modpack modpack; private final ServerModpackManifest manifest; - private final String name; + private final GameInstanceID instanceId; private final DefaultGameRepository repository; private final List> dependencies = new ArrayList<>(); private final List> dependents = new ArrayList<>(4); - public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, ServerModpackManifest manifest, String name) { + public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, Modpack modpack, ServerModpackManifest manifest, GameInstanceID instanceId) { this.zipFile = zipFile; this.modpack = modpack; this.manifest = manifest; - this.name = name; + this.instanceId = instanceId; this.repository = dependencyManager.getGameRepository(); - Path run = repository.getRunDirectory(name); + Path run = repository.getRunDirectory(instanceId); - Path json = repository.getModpackConfiguration(name); - if (repository.hasVersion(name) && Files.notExists(json)) - throw new IllegalArgumentException("Version " + name + " already exists."); + Path json = repository.getModpackConfiguration(instanceId); + if (repository.hasInstance(instanceId) && Files.notExists(json)) + throw new IllegalArgumentException("Version " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.gameBuilder().name(name); + GameBuilder builder = dependencyManager.newGameBuilder().name(instanceId); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { builder.version(addon.getId(), addon.getVersion()); } @@ -65,7 +66,7 @@ public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, dependents.add(builder.buildAsync()); onDone().register(event -> { if (event.isFailed()) - repository.removeVersionFromDisk(name); + repository.removeInstanceFromDisk(instanceId); }); ModpackConfiguration config = null; @@ -74,12 +75,12 @@ public ServerModpackLocalInstallTask(DefaultDependencyManager dependencyManager, config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ServerModpackManifest.class)); if (!ServerModpackProvider.INSTANCE.getName().equals(config.getType())) - throw new IllegalArgumentException("Version " + name + " is not a Server modpack. Cannot update this version."); + throw new IllegalArgumentException("Version " + instanceId + " is not a Server modpack. Cannot update this version."); } } catch (JsonParseException | IOException ignore) { } dependents.add(new ModpackInstallTask<>(zipFile, run, modpack.getEncoding(), Collections.singletonList("/overrides"), any -> true, config).withStage("hmcl.modpack")); - dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, ServerModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(name)).withStage("hmcl.modpack")); + dependents.add(new MinecraftInstanceTask<>(zipFile, modpack.getEncoding(), Collections.singletonList("/overrides"), manifest, ServerModpackProvider.INSTANCE, modpack.getName(), modpack.getVersion(), repository.getModpackConfiguration(instanceId)).withStage("hmcl.modpack")); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackManifest.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackManifest.java index f706b0add45..41a44b7b1b1 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackManifest.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackManifest.java @@ -19,6 +19,7 @@ import com.google.gson.JsonParseException; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackConfiguration; import org.jackhuang.hmcl.modpack.ModpackManifest; @@ -126,8 +127,8 @@ public Modpack toModpack(Charset encoding) throws IOException { .orElseThrow(() -> new IOException("Cannot find game version")).getVersion(); return new Modpack(name, author, version, gameVersion, description, encoding, this) { @Override - public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, String name, String iconUrl) { - return new ServerModpackLocalInstallTask(dependencyManager, zipFile, this, ServerModpackManifest.this, name); + public Task getInstallTask(DefaultDependencyManager dependencyManager, Path zipFile, GameInstanceID instanceId, String iconUrl) { + return new ServerModpackLocalInstallTask(dependencyManager, zipFile, this, ServerModpackManifest.this, instanceId); } }; } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackProvider.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackProvider.java index 01917c15d77..76e98f7fddf 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackProvider.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackProvider.java @@ -20,6 +20,7 @@ import com.google.gson.JsonParseException; import kala.compress.archivers.zip.ZipArchiveReader; import org.jackhuang.hmcl.download.DefaultDependencyManager; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.MismatchedModpackTypeException; import org.jackhuang.hmcl.modpack.Modpack; import org.jackhuang.hmcl.modpack.ModpackProvider; @@ -41,16 +42,16 @@ public String getName() { } @Override - public Task createCompletionTask(DefaultDependencyManager dependencyManager, String version) { - return new ServerModpackCompletionTask(dependencyManager, version); + public Task createCompletionTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId) { + return new ServerModpackCompletionTask(dependencyManager, instanceId); } @Override - public Task createUpdateTask(DefaultDependencyManager dependencyManager, String name, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { + public Task createUpdateTask(DefaultDependencyManager dependencyManager, GameInstanceID instanceId, Path zipFile, Modpack modpack) throws MismatchedModpackTypeException { if (!(modpack.getManifest() instanceof ServerModpackManifest serverModpackManifest)) throw new MismatchedModpackTypeException(getName(), modpack.getManifest().getProvider().getName()); - return new ModpackUpdateTask(dependencyManager.getGameRepository(), name, new ServerModpackLocalInstallTask(dependencyManager, zipFile, modpack, serverModpackManifest, name)); + return new ModpackUpdateTask(dependencyManager.getGameRepository(), instanceId, new ServerModpackLocalInstallTask(dependencyManager, zipFile, modpack, serverModpackManifest, instanceId)); } @Override diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java index ecff6c17ac6..e29f8438691 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/modpack/server/ServerModpackRemoteInstallTask.java @@ -21,6 +21,7 @@ import org.jackhuang.hmcl.download.DefaultDependencyManager; import org.jackhuang.hmcl.download.GameBuilder; import org.jackhuang.hmcl.game.DefaultGameRepository; +import org.jackhuang.hmcl.game.GameInstanceID; import org.jackhuang.hmcl.modpack.ModpackConfiguration; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.gson.JsonUtils; @@ -34,24 +35,24 @@ public class ServerModpackRemoteInstallTask extends Task { - private final String name; + private final GameInstanceID instanceId; private final DefaultDependencyManager dependency; private final DefaultGameRepository repository; private final List> dependencies = new ArrayList<>(1); private final List> dependents = new ArrayList<>(1); private final ServerModpackManifest manifest; - public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, ServerModpackManifest manifest, String name) { - this.name = name; + public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager, ServerModpackManifest manifest, GameInstanceID instanceId) { + this.instanceId = instanceId; this.dependency = dependencyManager; this.repository = dependencyManager.getGameRepository(); this.manifest = manifest; - Path json = repository.getModpackConfiguration(name); - if (repository.hasVersion(name) && Files.notExists(json)) - throw new IllegalArgumentException("Version " + name + " already exists."); + Path json = repository.getModpackConfiguration(instanceId); + if (repository.hasInstance(instanceId) && Files.notExists(json)) + throw new IllegalArgumentException("Instance " + instanceId + " already exists."); - GameBuilder builder = dependencyManager.gameBuilder().name(name); + GameBuilder builder = dependencyManager.newGameBuilder().name(instanceId); for (ServerModpackManifest.Addon addon : manifest.getAddons()) { builder.version(addon.getId(), addon.getVersion()); } @@ -59,7 +60,7 @@ public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager dependents.add(builder.buildAsync()); onDone().register(event -> { if (event.isFailed()) - repository.removeVersionFromDisk(name); + repository.removeInstanceFromDisk(instanceId); }); ModpackConfiguration config; @@ -68,7 +69,7 @@ public ServerModpackRemoteInstallTask(DefaultDependencyManager dependencyManager config = JsonUtils.fromJsonFile(json, ModpackConfiguration.typeOf(ServerModpackManifest.class)); if (!MODPACK_TYPE.equals(config.getType())) - throw new IllegalArgumentException("Version " + name + " is not a Server modpack. Cannot update this version."); + throw new IllegalArgumentException("Instance " + instanceId + " is not a Server modpack. Cannot update this instance."); } } catch (JsonParseException | IOException ignore) { } @@ -86,7 +87,7 @@ public List> getDependencies() { @Override public void execute() throws Exception { - dependencies.add(new ServerModpackCompletionTask(dependency, name, new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList()))); + dependencies.add(new ServerModpackCompletionTask(dependency, instanceId, new ModpackConfiguration<>(manifest, MODPACK_TYPE, manifest.getName(), manifest.getVersion(), Collections.emptyList()))); } public static final String MODPACK_TYPE = "Server"; diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ImmutableSequencedMap.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ImmutableSequencedMap.java new file mode 100644 index 00000000000..da68dd5aff2 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/ImmutableSequencedMap.java @@ -0,0 +1,205 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.util; + +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnknownNullability; +import org.jetbrains.annotations.Unmodifiable; + +import java.util.*; +import java.util.function.BiConsumer; + +/// An unmodifiable [Map] that preserves insertion order. +/// +/// This class wrap a [LinkedHashMap] and exposes only read operations; +/// any attempt to modify the map through the standard [Map] mutator methods +/// throws [UnsupportedOperationException]. +/// The iteration order of keys, values, and entries is always the order in which +/// they were originally inserted. +/// +/// Use the static factory methods [`of()`](#of()) and [`copyOf(Map)`](#copyOf(java.util.Map)) +/// to obtain instances. +/// +/// @param the type of keys +/// @param the type of values +@NotNullByDefault +public final class ImmutableSequencedMap + implements Map { + + /// Shared empty instance. + private static final ImmutableSequencedMap EMPTY = new ImmutableSequencedMap<>(new LinkedHashMap<>()); + + /// Returns the empty immutable sequenced map. + /// + /// @param the type of keys + /// @param the type of values + /// @return an empty [ImmutableSequencedMap] + @SuppressWarnings("unchecked") + public static ImmutableSequencedMap of() { + return (ImmutableSequencedMap) EMPTY; + } + + /// Returns an immutable sequenced map containing the entries of the given map + /// in their original iteration order. + /// + /// The entries are copied into an internal [LinkedHashMap], + /// so subsequent changes to the source map do not affect the returned map. + /// + /// @param the type of keys + /// @param the type of values + /// @param map the source map whose entries are copied + /// @return an [ImmutableSequencedMap] with the same entries and order as {@code map} + @SuppressWarnings("unchecked") + public static ImmutableSequencedMap copyOf(Map map) { + if (map instanceof ImmutableSequencedMap) { + return (ImmutableSequencedMap) map; + } + + if (map.isEmpty()) { + return of(); + } + + return new ImmutableSequencedMap<>(new LinkedHashMap<>(map)); + } + + /// Wraps the given [LinkedHashMap] directly without copying. + /// + /// The caller **must not** modify the map after passing it to this method; + /// the resulting [ImmutableSequencedMap] does not + /// make a defensive copy. + /// + /// @param the type of keys + /// @param the type of values + /// @param map the linked hash map to wrap + /// @return an [ImmutableSequencedMap] backed by {@code map} + public static ImmutableSequencedMap wrap(@Unmodifiable LinkedHashMap map) { + return new ImmutableSequencedMap<>(map); + } + + /// The backing map that stores the entries in insertion order. + private final LinkedHashMap map; + + private ImmutableSequencedMap(LinkedHashMap map) { + this.map = map; + } + + /// {@inheritDoc} + @Override + public int size() { + return map.size(); + } + + /// {@inheritDoc} + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + /// {@inheritDoc} + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + /// {@inheritDoc} + @Override + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + /// {@inheritDoc} + @Override + public V get(Object key) { + return map.get(key); + } + + /// Always throws [UnsupportedOperationException] because this map is unmodifiable. + /// + /// @throws UnsupportedOperationException always + @Override + public @Nullable V put(K key, V value) { + throw new UnsupportedOperationException(); + } + + /// Always throws [UnsupportedOperationException] because this map is unmodifiable. + /// + /// @throws UnsupportedOperationException always + @Override + public V remove(Object key) { + throw new UnsupportedOperationException(); + } + + /// Always throws [UnsupportedOperationException] because this map is unmodifiable. + /// + /// @throws UnsupportedOperationException always + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(); + } + + /// Always throws [UnsupportedOperationException] because this map is unmodifiable. + /// + /// @throws UnsupportedOperationException always + @Override + public void clear() { + throw new UnsupportedOperationException(); + } + + /// {@inheritDoc} + @Override + public @Unmodifiable Set keySet() { + return map.keySet(); + } + + /// {@inheritDoc} + @Override + public @Unmodifiable Collection values() { + return map.values(); + } + + /// {@inheritDoc} + @Override + public @Unmodifiable Set> entrySet() { + return map.entrySet(); + } + + /// {@inheritDoc} + @Override + public void forEach(BiConsumer action) { + map.forEach(action); + } + + /// {@inheritDoc} + @Override + public boolean equals(@Nullable Object obj) { + return obj == this || map.equals(obj); + } + + /// {@inheritDoc} + @Override + public int hashCode() { + return map.hashCode(); + } + + /// {@inheritDoc} + @Override + public String toString() { + return map.toString(); + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java index 0451c92ea90..3e6d66b625e 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/JsonUtils.java @@ -62,7 +62,7 @@ public final class JsonUtils { /// - Pretty printing enabled. /// - Complex map key serialization enabled. /// - Type adapters for [java.time.Instant], [java.util.UUID], and [java.nio.file.Path]. - /// - [ValidationTypeAdapterFactory], [LowerCaseEnumTypeAdapterFactory], and + /// - [ValidationTypeAdapterFactory], [LowerCaseEnumTypeAdapter#FACTORY], and /// [JsonTypeAdapterFactory]. public static final Gson GSON = defaultGsonBuilder().create(); @@ -72,11 +72,11 @@ public final class JsonUtils { /// Configured with: /// - [JsonTypeAdapterFactory] /// - [ValidationTypeAdapterFactory] - /// - [LowerCaseEnumTypeAdapterFactory] + /// - [LowerCaseEnumTypeAdapter#FACTORY] public static final Gson UGLY_GSON = new GsonBuilder() .registerTypeAdapterFactory(JsonTypeAdapterFactory.INSTANCE) .registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE) - .registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE) + .registerTypeAdapterFactory(LowerCaseEnumTypeAdapter.FACTORY) .create(); /// Not instantiable. @@ -651,7 +651,7 @@ public static void writeToJsonFile(Path file, @Nullable Object value) throws IOE /// - [InstantTypeAdapter] registered for [java.time.Instant]. /// - [UUIDTypeAdapter] registered for [java.util.UUID]. /// - [PathTypeAdapter] registered for [java.nio.file.Path]. - /// - [ValidationTypeAdapterFactory], [LowerCaseEnumTypeAdapterFactory], and + /// - [ValidationTypeAdapterFactory], [LowerCaseEnumTypeAdapter#FACTORY], and /// [JsonTypeAdapterFactory] registered as type adapter factories. /// /// Callers may further customize the returned builder before calling @@ -666,7 +666,7 @@ public static GsonBuilder defaultGsonBuilder() { .registerTypeAdapter(UUID.class, UUIDTypeAdapter.INSTANCE) .registerTypeAdapter(Path.class, PathTypeAdapter.INSTANCE) .registerTypeAdapterFactory(ValidationTypeAdapterFactory.INSTANCE) - .registerTypeAdapterFactory(LowerCaseEnumTypeAdapterFactory.INSTANCE) + .registerTypeAdapterFactory(LowerCaseEnumTypeAdapter.FACTORY) .registerTypeAdapterFactory(JsonTypeAdapterFactory.INSTANCE); } } diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/LowerCaseEnumTypeAdapter.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/LowerCaseEnumTypeAdapter.java new file mode 100644 index 00000000000..4ae0f90ec73 --- /dev/null +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/LowerCaseEnumTypeAdapter.java @@ -0,0 +1,138 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.util.gson; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +/// Serializes enum constants as lower-case enum names and reads enum names case-insensitively. +/// +/// Unknown values are deserialized as `null` for compatibility with legacy code that treated +/// unsupported enum values as absent. +/// +/// TODO: Annotate enum types that should be serialized as lower-case names to use this adapter. +@NotNullByDefault +public final class LowerCaseEnumTypeAdapter> extends TypeAdapter<@Nullable E> { + + /// Gson factory that applies [LowerCaseEnumTypeAdapter] to enum raw types. + public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() { + /// Creates a lower-case enum adapter for enum types, or returns `null` for non-enum types. + /// + /// @param gson the Gson instance requesting the adapter + /// @param type the target type token + /// @return an enum adapter for enum raw types, or `null` to let Gson continue factory lookup + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public @Nullable TypeAdapter create(Gson gson, TypeToken type) { + Class rawType = type.getRawType(); + return rawType.isEnum() ? new LowerCaseEnumTypeAdapter(rawType) : null; + } + }; + + /// Maps lower-case enum names to their corresponding constants for lookup during deserialization. + private final @Unmodifiable Map lowercaseToConstant; + + /// Creates an adapter for the supplied enum class. + /// + /// @param enumClass the enum class whose constants are accepted by this adapter + private LowerCaseEnumTypeAdapter(Class enumClass) { + var lowercaseToConstant = new HashMap(); + for (E constant : enumClass.getEnumConstants()) { + lowercaseToConstant.put(constant.name().toLowerCase(Locale.ROOT), constant); + } + this.lowercaseToConstant = Map.copyOf(lowercaseToConstant); + } + + /// Reads a nullable enum value from the next JSON token. + /// + /// `null` JSON tokens are returned as `null`; string tokens are matched against enum names + /// case-insensitively. Unknown names are also returned as `null` for compatibility. + /// + /// @param in the JSON reader positioned at the enum value + /// @return the enum constant represented by the token, or `null` for JSON null or unknown names + /// @throws IOException if the reader cannot read the next token + @Override + public @Nullable E read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + String name = in.nextString(); + return lowercaseToConstant.get(name.toLowerCase(Locale.ROOT)); + } + + /// Writes a nullable enum value to JSON. + /// + /// Non-null values are written as lower-case enum names; `null` values are written as JSON null. + /// + /// @param out the JSON writer receiving the value + /// @param value the enum constant to write, or `null` + /// @throws IOException if the writer cannot write the value + @Override + public void write(JsonWriter out, @Nullable E value) throws IOException { + out.value(value != null ? value.name().toLowerCase(Locale.ROOT) : null); + } + + /// Converts a JSON primitive into an enum constant by matching enum names case-insensitively. + /// + /// @param clazz the enum class to search + /// @param json the JSON primitive containing the enum name + /// @return the enum constant represented by `json`, or `null` when the primitive is not a string + /// or names no enum constant + public static > @Nullable E fromJson(Class clazz, JsonElement json) { + return json instanceof JsonPrimitive primitive && primitive.isString() + ? fromJson(clazz, json.getAsString()) + : null; + + } + + /// Converts a JSON primitive into an enum constant by matching enum names case-insensitively. + /// + /// @param clazz the enum class to search + /// @param name the JSON string value + /// @return the enum constant represented by `json`, or `null` when the primitive is not a string + /// or names no enum constant + public static > @Nullable E fromJson(Class clazz, String name) { + for (E constant : clazz.getEnumConstants()) { + if (constant.name().equalsIgnoreCase(name)) + return constant; + } + + return null; + } + + /// Converts an enum constant into its lower-case JSON representation. + /// + /// @param value the enum constant to convert, or `null` + /// @return a lower-case string primitive for non-null values, or [JsonNull#INSTANCE] for `null` + public static > JsonElement toJsonTree(@Nullable E value) { + return value != null ? new JsonPrimitive(value.name().toLowerCase(Locale.ROOT)) : JsonNull.INSTANCE; + } +} diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/LowerCaseEnumTypeAdapterFactory.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/LowerCaseEnumTypeAdapterFactory.java deleted file mode 100644 index 030f2e66a7a..00000000000 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/gson/LowerCaseEnumTypeAdapterFactory.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Hello Minecraft! Launcher - * Copyright (C) 2020 huangyuhui 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 . - */ -package org.jackhuang.hmcl.util.gson; - -import com.google.gson.Gson; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonToken; -import com.google.gson.stream.JsonWriter; - -import java.io.IOException; -import java.util.HashMap; -import java.util.Locale; - -/** - * - * @author huangyuhui - */ -public final class LowerCaseEnumTypeAdapterFactory implements TypeAdapterFactory { - - public static final LowerCaseEnumTypeAdapterFactory INSTANCE = new LowerCaseEnumTypeAdapterFactory(); - - @Override - @SuppressWarnings("unchecked") - public TypeAdapter create(Gson gson, TypeToken tt) { - Class rawType = tt.getRawType(); - if (!rawType.isEnum()) - return null; - - HashMap lowercaseToConstant = new HashMap<>(); - for (Object constant : rawType.getEnumConstants()) - lowercaseToConstant.put(toLowercase(constant), (T) constant); - - return new TypeAdapter() { - @Override - public void write(JsonWriter writer, T t) throws IOException { - if (t == null) - writer.nullValue(); - else - writer.value(toLowercase(t)); - } - - @Override - public T read(JsonReader reader) throws IOException { - if (reader.peek() == JsonToken.NULL) { - reader.nextNull(); - return null; - } - return lowercaseToConstant.get(reader.nextString().toLowerCase(Locale.ROOT)); - } - }; - } - - private static String toLowercase(Object o) { - return o.toString().toLowerCase(Locale.ROOT); - } -} diff --git a/HMCLCore/src/test/java/org/jackhuang/hmcl/game/GameInstanceManifestTest.java b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/GameInstanceManifestTest.java new file mode 100644 index 00000000000..f42b58d5ce6 --- /dev/null +++ b/HMCLCore/src/test/java/org/jackhuang/hmcl/game/GameInstanceManifestTest.java @@ -0,0 +1,219 @@ +/* + * Hello Minecraft! Launcher + * Copyright (C) 2026 huangyuhui 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 . + */ +package org.jackhuang.hmcl.game; + +import com.google.gson.JsonObject; +import org.jetbrains.annotations.NotNullByDefault; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/// Tests for game instance manifest parsing and resolution behavior. +@NotNullByDefault +public final class GameInstanceManifestTest { + + /// Ordinary manifest edits preserve absent nullable Boolean fields. + @Test + public void testNullableBooleanFieldsAreNotMaterialized() { + GameInstanceManifest manifest = manifest("example", null, null, null, null) + .withJar(new GameInstanceID("parent")); + + JsonObject json = manifest.toJsonObject(); + assertFalse(json.has("root")); + assertFalse(json.has("hidden")); + } + + /// Root manifests with patch lists resolve from the patch view instead of their own body fields. + @Test + public void testRootManifestWithPatchesUsesPatchView() throws NoSuchGameInstanceException { + GameInstanceManifest manifest = manifest( + "example", + "example.Main", + true, + false, + List.of(patch("patch", null))); + + GameInstanceManifest.Resolved resolved = new DefaultGameRepository(Path.of(".")).resolve(manifest); + + assertNull(resolved.launchManifest().mainClass()); + assertNull(resolved.launchManifest().patches()); + assertEquals(List.of(patch("patch", null)), resolved.standaloneManifest().getPatches()); + } + + /// Manifest edits update known raw JSON fields without discarding unknown fields. + @Test + public void testRawJsonIsPreservedWhenUpdatingKnownFields() { + JsonObject json = new JsonObject(); + json.addProperty("id", "example"); + json.addProperty("root", false); + json.addProperty("unknownField", "value"); + + GameInstanceManifest manifest = GameInstanceManifest.fromJson(json, true) + .withRoot(true) + .withMainClass("example.Main"); + + JsonObject updatedJson = manifest.toJsonObject(); + assertEquals("value", updatedJson.get("unknownField").getAsString()); + assertEquals(true, updatedJson.get("root").getAsBoolean()); + assertEquals("example.Main", updatedJson.get("mainClass").getAsString()); + + JsonObject rootRemovedJson = manifest.withRoot(null).toJsonObject(); + assertEquals("value", rootRemovedJson.get("unknownField").getAsString()); + assertFalse(rootRemovedJson.has("root")); + } + + /// Download maps keep the JSON entry order after parsing and rebuilding. + @Test + public void testDownloadMapOrderIsPreserved() { + JsonObject serverDownload = new JsonObject(); + serverDownload.addProperty("url", "server"); + JsonObject clientDownload = new JsonObject(); + clientDownload.addProperty("url", "client"); + + JsonObject downloads = new JsonObject(); + downloads.add("SERVER", serverDownload); + downloads.add("CLIENT", clientDownload); + + JsonObject json = new JsonObject(); + json.addProperty("id", "example"); + json.add("downloads", downloads); + + GameInstanceManifest manifest = GameInstanceManifest.fromJson(json, true); + assertEquals(List.of(DownloadType.SERVER, DownloadType.CLIENT), List.copyOf(manifest.getDownloads().keySet())); + + JsonObject updatedDownloads = manifest.withDownloads(manifest.getDownloads()) + .toJsonObject() + .getAsJsonObject("downloads"); + assertEquals(List.of("SERVER", "CLIENT"), List.copyOf(updatedDownloads.keySet())); + } + + /// Patch edits preserve raw JSON fields and typed inherited instance ids. + @Test + public void testPatchParsingAndCopyBehavior() { + JsonObject json = new JsonObject(); + json.addProperty("id", "patch"); + json.addProperty("mainClass", "old.Main"); + json.addProperty("inheritsFrom", "parent"); + json.addProperty("unknownField", "value"); + + GameInstancePatch originalPatch = GameInstancePatch.fromJson(json); + assertEquals(new GameInstanceID("parent"), originalPatch.inheritsFrom()); + assertSame(originalPatch, originalPatch.withId("patch")); + assertSame(originalPatch, originalPatch.withMainClass("old.Main")); + + GameInstancePatch patch = originalPatch + .withMainClass("new.Main") + .withId(null); + + JsonObject updatedJson = patch.toJsonObject(); + assertEquals("value", updatedJson.get("unknownField").getAsString()); + assertEquals("new.Main", updatedJson.get("mainClass").getAsString()); + assertEquals("parent", updatedJson.get("inheritsFrom").getAsString()); + assertFalse(updatedJson.has("id")); + + JsonObject serverDownload = new JsonObject(); + serverDownload.addProperty("url", "server"); + JsonObject clientDownload = new JsonObject(); + clientDownload.addProperty("url", "client"); + + JsonObject downloads = new JsonObject(); + downloads.add("SERVER", serverDownload); + downloads.add("CLIENT", clientDownload); + + JsonObject patchJson = new JsonObject(); + patchJson.addProperty("id", "patch"); + patchJson.add("downloads", downloads); + + GameInstancePatch patchWithDownloads = GameInstancePatch.fromJson(patchJson); + Map patchDownloads = Objects.requireNonNull(patchWithDownloads.downloads()); + assertEquals(List.of(DownloadType.SERVER, DownloadType.CLIENT), List.copyOf(patchDownloads.keySet())); + + JsonObject updatedDownloads = patchWithDownloads.withDownload(patchDownloads) + .toJsonObject() + .getAsJsonObject("downloads"); + assertEquals(List.of("SERVER", "CLIENT"), List.copyOf(updatedDownloads.keySet())); + } + + /// Creates a minimal manifest for tests. + private static GameInstanceManifest manifest( + String id, + @Nullable String mainClass, + @Nullable Boolean root, + @Nullable Boolean hidden, + @Nullable List patches) { + return new GameInstanceManifest( + new GameInstanceID(id), + null, + null, + mainClass, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + root, + hidden, + patches, + null); + } + + /// Creates a minimal patch for tests. + private static GameInstancePatch patch(String id, @Nullable String mainClass) { + return new GameInstancePatch( + id, + null, + 0, + null, + null, + mainClass, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null); + } +}