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 4009895644..23df78538f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/Controllers.java @@ -27,16 +27,11 @@ import org.jackhuang.hmcl.game.LauncherHelper; import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.java.JavaManager; import org.jackhuang.hmcl.java.JavaRuntime; -import org.glavo.url.WebURL; import org.jackhuang.hmcl.modpack.Modpack; -import org.jackhuang.hmcl.modpack.server.ServerModpackManifest; import org.jackhuang.hmcl.setting.*; -import org.jackhuang.hmcl.task.GetTask; import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.TaskExecutor; -import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; -import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.ui.account.AccountListPage; import org.jackhuang.hmcl.ui.animation.ContainerAnimations; import org.jackhuang.hmcl.ui.construct.*; @@ -359,8 +354,8 @@ public final class Controllers { ); } - /// Installs a launcher-bundled modpack when present; otherwise, when the repository has no - /// instances yet, pulls the org client [Metadata#getClientManifestUrl] and opens the install wizard. + /// Installs a launcher-bundled modpack when present. First-run remote install is deferred to the + /// home Start button ([Instances#ensureClientAndJoin]) so players never enter the install wizard. private static void tryInstallBundledModpack(HMCLGameRepository repository) { @Nullable Path modpackFile = Metadata.findBundledModpackFile(); if (modpackFile != null) { @@ -384,35 +379,7 @@ public final class Controllers { } }), i18n("modpack.installing"), TaskCancellationAction.NO_CANCEL ); - return; } - - if (repository.getDisplayInstances().findAny().isPresent()) { - return; - } - - String manifestUrl = Metadata.getClientManifestUrl(); - LOG.info("No instances yet; fetching client Server Modpack from " + manifestUrl); - Controllers.taskDialog( - new GetTask(WebURL.parse(manifestUrl)) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null) { - LOG.warning("Failed to fetch client Server Modpack manifest", exception); - return; - } - ServerModpackManifest manifest = - JsonUtils.fromMaybeMalformedJson(result, ServerModpackManifest.class); - if (manifest == null) { - LOG.warning("Malformed client Server Modpack manifest from " + manifestUrl); - return; - } - Controllers.getDecorator().startWizard( - new ModpackInstallWizardProvider(repository, manifest), - i18n("install.modpack")); - }), - i18n("message.downloading"), - TaskCancellationAction.NO_CANCEL - ); } /// Shows the content in the main-window dialog stack without waiting for it to close. diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java index 232ad88c51..7a50d724c2 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/instances/Instances.java @@ -25,13 +25,14 @@ import org.jackhuang.hmcl.ui.construct.DialogCloseEvent; import org.jackhuang.hmcl.ui.construct.MessageDialogPane; import org.jackhuang.hmcl.ui.construct.PromptDialogPane; import org.jackhuang.hmcl.ui.construct.Validator; -import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; +import org.jackhuang.hmcl.ui.download.LocalModpackPage; import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.TaskCancellationAction; import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.i18n.I18n; import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.platform.OperatingSystem; +import org.jackhuang.hmcl.modpack.Modpack; import org.jetbrains.annotations.Nullable; import java.io.IOException; @@ -48,63 +49,158 @@ public final class Instances { private Instances() { } - /// Installs or updates the TDLM client. + /// Installs or updates the TDLM client from fileApi / bundled zip without opening the install UI, + /// then joins the configured server when ready. /// - /// Prefers a launcher-bundled zip, then the org [Metadata#getClientManifestUrl] Server Modpack, - /// then the generic local/remote import wizard. - public static void installServerModpack() { + /// Prefer this for the home Start / Join button. Existing server-modpack instances get differential + /// updates during launch via [org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask]. + public static void ensureClientAndJoin() { HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); if (!repository.isLoaded()) { return; } + // Already installed: join immediately. Launch runs ServerModpackCompletionTask for updates. + if (repository.getSelectedInstance() != null && Metadata.findBundledModpackFile() == null) { + joinServer(repository.getSelectedInstance()); + return; + } + Path bundled = Metadata.findBundledModpackFile(); if (bundled != null) { - Controllers.getDecorator().startWizard( - new ModpackInstallWizardProvider(repository, bundled), - i18n("install.modpack")); + installBundledThenJoin(repository, bundled); return; } Controllers.taskDialog( new GetTask(WebURL.parse(Metadata.getClientManifestUrl())) - .whenComplete(Schedulers.javafx(), (result, exception) -> { - if (exception != null) { - Controllers.dialog( - exception.getMessage(), - i18n("message.error"), - MessageDialogPane.MessageType.ERROR); - importModpack(); - return; - } + .thenComposeAsync(Schedulers.javafx(), result -> { ServerModpackManifest manifest = JsonUtils.fromMaybeMalformedJson( result, ServerModpackManifest.class); if (manifest == null) { + throw new IOException(i18n("modpack.type.server.malformed")); + } + return ensureRemoteClientInstalled(repository, manifest); + }) + .whenComplete(Schedulers.javafx(), (ignored, exception) -> { + if (exception != null) { + LOG.warning("Failed to install or update TDLM client", exception); Controllers.dialog( - i18n("modpack.type.server.malformed"), - i18n("message.error"), + StringUtils.getStackTrace(exception), + i18n("install.failed"), MessageDialogPane.MessageType.ERROR); - importModpack(); return; } - Controllers.getDecorator().startWizard( - new ModpackInstallWizardProvider(repository, manifest), - i18n("install.modpack")); + joinServer(GameDirectoryManager.getSelectedRepository().getSelectedInstance()); }), - i18n("message.downloading"), + i18n("modpack.installing"), TaskCancellationAction.NORMAL); } - public static void importModpack() { - HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); - if (repository.isLoaded()) { - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(repository), i18n("install.modpack")); - } + /// Silent first-install / update from the org client manifest — no wizard UI. + public static void installServerModpack() { + ensureClientAndJoin(); } - /// @deprecated use [#installServerModpack()] + /// Install wizard is disabled for the dedicated server-client launcher. + public static void importModpack() { + LOG.info("Ignoring importModpack: install UI is disabled; use Start to install/update"); + } + + /// @deprecated use [#ensureClientAndJoin()] public static void addNewGame() { - installServerModpack(); + ensureClientAndJoin(); + } + + /// Installs a local modpack zip without the wizard, then selects the new instance. + public static void installLocalModpackSilently(HMCLGameRepository repository, Path file) { + if (!repository.isLoaded()) { + return; + } + Controllers.taskDialog( + Task.composeAsync(Schedulers.io(), () -> { + Modpack modpack = ModpackHelper.readModpackManifest(file); + String name = LocalModpackPage.processModpackName(modpack.getName()); + if (StringUtils.isBlank(name)) { + name = "TDLM"; + } + GameInstanceID instanceId = new GameInstanceID(name); + if (repository.hasInstance(instanceId)) { + instanceId = new GameInstanceID(name + "-" + System.currentTimeMillis()); + } + GameInstanceID id = instanceId; + return ModpackHelper.getInstallTask(repository, file, id, modpack, null) + .thenRunAsync(Schedulers.javafx(), () -> + repository.setSelectedInstance(repository.getInstance(id))); + }) + .whenComplete(Schedulers.javafx(), (ignored, exception) -> { + if (exception != null) { + LOG.warning("Failed to install local modpack", exception); + Controllers.dialog( + StringUtils.getStackTrace(exception), + i18n("install.failed"), + MessageDialogPane.MessageType.ERROR); + } + }), + i18n("modpack.installing"), + TaskCancellationAction.NORMAL); + } + + private static void installBundledThenJoin(HMCLGameRepository repository, Path bundled) { + Controllers.taskDialog( + Task.composeAsync(Schedulers.io(), () -> { + Modpack modpack = ModpackHelper.readModpackManifest(bundled); + String name = LocalModpackPage.processModpackName(modpack.getName()); + if (StringUtils.isBlank(name)) { + name = "TDLM"; + } + GameInstanceID instanceId = new GameInstanceID(name); + return ModpackHelper.getInstallTask(repository, bundled, instanceId, modpack, null) + .thenRunAsync(Schedulers.javafx(), () -> { + repository.setSelectedInstance(repository.getInstance(instanceId)); + try { + Files.deleteIfExists(bundled); + } catch (IOException e) { + LOG.warning("Failed to delete bundled modpack: " + bundled, e); + } + }); + }) + .whenComplete(Schedulers.javafx(), (ignored, exception) -> { + if (exception != null) { + LOG.warning("Failed to install bundled modpack", exception); + Controllers.dialog( + StringUtils.getStackTrace(exception), + i18n("install.failed"), + MessageDialogPane.MessageType.ERROR); + return; + } + joinServer(GameDirectoryManager.getSelectedRepository().getSelectedInstance()); + }), + i18n("modpack.installing"), + TaskCancellationAction.NO_CANCEL); + } + + /// Installs when missing; selects an existing instance otherwise. + /// + /// Differential updates for an installed server modpack run during launch via + /// [org.jackhuang.hmcl.modpack.server.ServerModpackCompletionTask]. + private static Task ensureRemoteClientInstalled( + HMCLGameRepository repository, ServerModpackManifest manifest) throws IOException { + Modpack modpack = manifest.toModpack(); + String name = LocalModpackPage.processModpackName(manifest.getName()); + if (StringUtils.isBlank(name)) { + name = "TDLM"; + } + GameInstanceID instanceId = new GameInstanceID(name); + + if (repository.hasInstance(instanceId)) { + repository.setSelectedInstance(repository.getInstance(instanceId)); + return Task.completed(null); + } + + return ModpackHelper.getInstallTask(repository, manifest, instanceId, modpack) + .thenRunAsync(Schedulers.javafx(), () -> + repository.setSelectedInstance(repository.getInstance(instanceId))); } public static void deleteInstance(HMCLGameInstance gameInstance) { @@ -257,8 +353,10 @@ public final class Instances { .addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("instance.manage.duplicate.duplicate_save"), false))); } + /// Updates via fileApi through the Start flow — no install wizard. public static void updateInstance(HMCLGameInstance gameInstance) { - Controllers.getDecorator().startWizard(new ModpackInstallWizardProvider(gameInstance.getRepository(), gameInstance.getId())); + gameInstance.getRepository().setSelectedInstance(gameInstance); + ensureClientAndJoin(); } /// Queues an asset check with a fresh asset index for the supplied instance. @@ -338,14 +436,7 @@ public final class Instances { @SafeVarargs public static void launch(@Nullable HMCLGameInstance gameInstance, Consumer... injecters) { if (gameInstance == null) { - JFXButton installModpack = new JFXButton(i18n("install.modpack")); - installModpack.getStyleClass().add("dialog-accept"); - installModpack.setOnAction(e -> installServerModpack()); - - Controllers.confirmAction(i18n("instance.empty.launch"), i18n("launch.failed"), - MessageDialogPane.MessageType.ERROR, - installModpack, - null); + ensureClientAndJoin(); return; } @@ -412,11 +503,10 @@ public final class Instances { Controllers.navigate(Controllers.getSettingsPage()); } + /// Instance management UI is disabled for the dedicated server-client launcher. public static void modifyGameSettings(HMCLGameInstance gameInstance) { - Controllers.getGameInstancePage().setInstance(gameInstance.getId(), gameInstance.getRepository()); - Controllers.getGameInstancePage().showInstanceSettings(); - // VersionPage.loadVersion will be invoked after navigation - Controllers.navigate(Controllers.getGameInstancePage()); + LOG.info("Ignoring modifyGameSettings for " + gameInstance.getId() + + ": instance management UI is disabled"); } } 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 003aad6206..383b647638 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 @@ -135,18 +135,20 @@ public final class MainPage extends StackPane implements DecoratorPage { card.setMaxHeight(Region.USE_PREF_SIZE); StackPane.setAlignment(card, Pos.BOTTOM_CENTER); + // Start always installs/updates from fileApi then joins — never opens install or instance UI. + joinButton.setText(i18n("server.join")); + FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launch); + FXUtils.onChangeAndOperate(currentGameProperty(), currentGame -> { if (currentGame == null) { - joinButton.setText(i18n("instance.launch.empty")); - FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launchNoGame); packValue.setText(i18n("home.not_installed")); routeFact.setVisible(false); routeFact.setManaged(false); serverFact.setVisible(false); serverFact.setManaged(false); + routeFact.setOnMouseClicked(null); + routeFact.setCursor(Cursor.DEFAULT); } else { - joinButton.setText(i18n("server.join")); - FXUtils.setOnActionWithCooldown(joinButton, MainPage.this::launch); packValue.setText(currentGame.getId().toString()); ServerEndpoint.@Nullable Route route = ServerEndpoint.selected(currentGame.getModpackConfigurationFile()); routeFact.setVisible(route != null); @@ -214,14 +216,9 @@ public final class MainPage extends StackPane implements DecoratorPage { return label; } + /// Installs or updates the client from fileApi when needed, then launches into the server. private void launch() { - HMCLGameRepository repository = GameDirectoryManager.getSelectedRepository(); - Instances.joinServer(repository.getSelectedInstance()); - } - - /// Opens the server modpack install/update flow when no instance is selected. - private void launchNoGame() { - Instances.installServerModpack(); + Instances.ensureClientAndJoin(); } @Override 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 22ff961f62..058f40da86 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 @@ -4,7 +4,6 @@ package org.jackhuang.hmcl.ui.main; import com.jfoenix.controls.JFXPopup; import javafx.beans.property.ReadOnlyObjectProperty; -import org.jackhuang.hmcl.game.HMCLGameInstance; import org.jackhuang.hmcl.game.ModpackHelper; import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.GameDirectoryManager; @@ -17,8 +16,6 @@ import org.jackhuang.hmcl.ui.account.AccountListPopupMenu; import org.jackhuang.hmcl.ui.construct.AdvancedListItem; import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage; import org.jackhuang.hmcl.ui.decorator.DecoratorPage; -import org.jackhuang.hmcl.ui.download.ModpackInstallWizardProvider; -import org.jackhuang.hmcl.ui.instances.GameAdvancedListItem; import org.jackhuang.hmcl.ui.instances.Instances; import org.jackhuang.hmcl.util.io.FileUtils; import org.jetbrains.annotations.Nullable; @@ -43,20 +40,7 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { accountListItem.setOnAction(e -> AccountListPopupMenu.show(accountListItem, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, 0, -accountListItem.getHeight())); accountListItem.accountProperty().bind(Accounts.selectedAccountProperty()); - // second item in left sidebar - GameAdvancedListItem gameInstanceItem = new GameAdvancedListItem(); - gameInstanceItem.setOnAction(e -> { - @Nullable HMCLGameInstance instance = GameDirectoryManager.getSelectedRepository().getSelectedInstance(); - if (instance == null) { - Instances.installServerModpack(); - } else { - Instances.modifyGameSettings(instance); - } - }); - if (AnimationUtils.isAnimationEnabled()) { - gameInstanceItem.setPrepareAction(Controllers::prepareGameInstancePage); - } - + // Instance management entry removed: Start on the home page installs/updates and joins. AdvancedListItem launcherSettingsItem = new AdvancedListItem(); launcherSettingsItem.setLeftIcon(SVG.SETTINGS); launcherSettingsItem.setTitle(i18n("settings")); @@ -68,9 +52,8 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { launcherSettingsItem.setPrepareAction(Controllers::prepareSettingsPage); } - // The sidebar holds the modpack and settings entries; the account sits at the bottom-left. + // Sidebar: settings only; account sits at the bottom-left. getSidebar() - .add(gameInstanceItem) .add(launcherSettingsItem); setSidebarBottom(accountListItem); @@ -92,9 +75,9 @@ public class RootPage extends DecoratorAnimatedPage implements DecoratorPage { modpacks -> { Path file = modpacks.get(0); if (ModpackHelper.isFileModpackByExtension(file)) { - Controllers.getDecorator().startWizard( - new ModpackInstallWizardProvider(GameDirectoryManager.getSelectedRepository(), file), - i18n("install.modpack")); + // Silent install — no modpack wizard UI. + Instances.installLocalModpackSilently( + GameDirectoryManager.getSelectedRepository(), file); } else if ("json".equalsIgnoreCase(FileUtils.getExtension(file))) { Instances.installFromJson(GameDirectoryManager.getSelectedRepository(), file); }