Compare commits

...
Author SHA1 Message Date
layue13 a19679fd00 ux: Start auto-installs client; hide install and instance UI
Mirror Repository / Mirror to CNB (push) Skipped
Mirror Repository / Mirror to Gitee (push) Skipped
Java CI / build (pull_request) Successful in 8m38s
HMC-22: the home Join/Start button runs fileApi install when needed,
then joins. Differential updates stay on the launch completion path.
Remove the instance-management sidebar entry and stop opening the
modpack install wizard from startup, empty launch, or drag-drop.
2026-11-10 05:01:58 +00:00
4 changed files with 149 additions and 112 deletions

No files matched your search

@@ -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.
@@ -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<LauncherHelper>... 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");
}
}
@@ -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
@@ -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);
}