feat: 添加快速进入世界功能 (#4872)

close #2563
This commit is contained in:
mineDiamond
2025-12-20 20:33:52 +08:00
committed by GitHub
parent 7f852080cd
commit 0d7ece819d
17 changed files with 168 additions and 56 deletions

View File

@@ -25,20 +25,20 @@ import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.download.LibraryAnalyzer; import org.jackhuang.hmcl.download.LibraryAnalyzer;
import org.jackhuang.hmcl.event.Event; import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager; import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.mod.ModAdviser; import org.jackhuang.hmcl.mod.ModAdviser;
import org.jackhuang.hmcl.mod.Modpack; import org.jackhuang.hmcl.mod.Modpack;
import org.jackhuang.hmcl.mod.ModpackConfiguration; import org.jackhuang.hmcl.mod.ModpackConfiguration;
import org.jackhuang.hmcl.mod.ModpackProvider; import org.jackhuang.hmcl.mod.ModpackProvider;
import org.jackhuang.hmcl.setting.Profile; import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.setting.VersionIconType; import org.jackhuang.hmcl.setting.VersionIconType;
import org.jackhuang.hmcl.setting.VersionSetting; import org.jackhuang.hmcl.setting.VersionSetting;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FileSaver;
import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils; import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils; import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.FileUtils; import org.jackhuang.hmcl.util.io.FileUtils;
import org.jackhuang.hmcl.java.JavaRuntime;
import org.jackhuang.hmcl.util.platform.OperatingSystem; import org.jackhuang.hmcl.util.platform.OperatingSystem;
import org.jackhuang.hmcl.util.platform.SystemInfo; import org.jackhuang.hmcl.util.platform.SystemInfo;
import org.jackhuang.hmcl.util.versioning.GameVersionNumber; import org.jackhuang.hmcl.util.versioning.GameVersionNumber;
@@ -55,8 +55,8 @@ import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.setting.ConfigHolder.config;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
import static org.jackhuang.hmcl.util.Pair.pair; import static org.jackhuang.hmcl.util.Pair.pair;
import static org.jackhuang.hmcl.util.logging.Logger.LOG;
public final class HMCLGameRepository extends DefaultGameRepository { public final class HMCLGameRepository extends DefaultGameRepository {
private final Profile profile; private final Profile profile;
@@ -418,7 +418,6 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setWidth(vs.getWidth()) .setWidth(vs.getWidth())
.setHeight(vs.getHeight()) .setHeight(vs.getHeight())
.setFullscreen(vs.isFullscreen()) .setFullscreen(vs.isFullscreen())
.setServerIp(vs.getServerIp())
.setWrapper(vs.getWrapper()) .setWrapper(vs.getWrapper())
.setPreLaunchCommand(vs.getPreLaunchCommand()) .setPreLaunchCommand(vs.getPreLaunchCommand())
.setPostExitCommand(vs.getPostExitCommand()) .setPostExitCommand(vs.getPostExitCommand())
@@ -435,6 +434,10 @@ public final class HMCLGameRepository extends DefaultGameRepository {
.setJavaAgents(javaAgents) .setJavaAgents(javaAgents)
.setJavaArguments(javaArguments); .setJavaArguments(javaArguments);
if (StringUtils.isNotBlank(vs.getServerIp())) {
builder.setQuickPlayOption(new QuickPlayOption.MultiPlayer(vs.getServerIp()));
}
if (config().hasProxy()) { if (config().hasProxy()) {
builder.setProxyType(config().getProxyType()); builder.setProxyType(config().getProxyType());
builder.setProxyHost(config().getProxyHost()); builder.setProxyHost(config().getProxyHost());

View File

@@ -52,6 +52,7 @@ import org.jackhuang.hmcl.util.versioning.VersionNumber;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.URI; import java.net.URI;
import java.nio.file.AccessDeniedException; import java.nio.file.AccessDeniedException;
@@ -61,7 +62,6 @@ import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.locks.ReentrantLock;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.lang.ref.WeakReference;
import static javafx.application.Platform.runLater; import static javafx.application.Platform.runLater;
import static javafx.application.Platform.setImplicitExit; import static javafx.application.Platform.setImplicitExit;
@@ -82,6 +82,7 @@ public final class LauncherHelper {
private final VersionSetting setting; private final VersionSetting setting;
private LauncherVisibility launcherVisibility; private LauncherVisibility launcherVisibility;
private boolean showLogs; private boolean showLogs;
private QuickPlayOption quickPlayOption;
private boolean disableOfflineSkin = false; private boolean disableOfflineSkin = false;
public LauncherHelper(Profile profile, Account account, String selectedVersion) { public LauncherHelper(Profile profile, Account account, String selectedVersion) {
@@ -113,6 +114,10 @@ public final class LauncherHelper {
launcherVisibility = LauncherVisibility.KEEP; launcherVisibility = LauncherVisibility.KEEP;
} }
public void setQuickPlayOption(QuickPlayOption quickPlayOption) {
this.quickPlayOption = quickPlayOption;
}
public void setDisableOfflineSkin() { public void setDisableOfflineSkin() {
disableOfflineSkin = true; disableOfflineSkin = true;
} }
@@ -199,6 +204,9 @@ public final class LauncherHelper {
if (disableOfflineSkin) { if (disableOfflineSkin) {
launchOptionsBuilder.setDaemon(false); launchOptionsBuilder.setDaemon(false);
} }
if (quickPlayOption != null) {
launchOptionsBuilder.setQuickPlayOption(quickPlayOption);
}
LaunchOptions launchOptions = launchOptionsBuilder.create(); 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(selectedVersion), 10));

View File

@@ -373,9 +373,8 @@ public class DecoratorController {
if (navigator.getCurrentPage() instanceof DecoratorPage) { if (navigator.getCurrentPage() instanceof DecoratorPage) {
DecoratorPage page = (DecoratorPage) navigator.getCurrentPage(); DecoratorPage page = (DecoratorPage) navigator.getCurrentPage();
// FIXME: Get WorldPage working first, and revisit this later
page.closePage();
if (page.isPageCloseable()) { if (page.isPageCloseable()) {
page.closePage();
return; return;
} }
} }

View File

@@ -346,7 +346,7 @@ public final class MainPage extends StackPane implements DecoratorPage {
private void launch() { private void launch() {
Profile profile = Profiles.getSelectedProfile(); Profile profile = Profiles.getSelectedProfile();
Versions.launch(profile, profile.getSelectedVersion(), null); Versions.launch(profile, profile.getSelectedVersion());
} }
private void launchNoGame() { private void launchNoGame() {

View File

@@ -85,10 +85,14 @@ public class GameListItem extends Control {
Versions.openFolder(profile, version); Versions.openFolder(profile, version);
} }
public void launch() { public void testGame() {
Versions.testGame(profile, version); Versions.testGame(profile, version);
} }
public void launch() {
Versions.launch(profile, version);
}
public void modifyGameSettings() { public void modifyGameSettings() {
Versions.modifyGameSettings(profile, version); Versions.modifyGameSettings(profile, version);
} }

View File

@@ -44,7 +44,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
JFXPopup popup = new JFXPopup(menu); JFXPopup popup = new JFXPopup(menu);
menu.getContent().setAll( menu.getContent().setAll(
new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), () -> currentSkinnable.launch(), popup), new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch.test"), () -> currentSkinnable.testGame(), popup),
new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> currentSkinnable.generateLaunchScript(), popup), new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), () -> currentSkinnable.generateLaunchScript(), popup),
new MenuSeparator(), new MenuSeparator(),
new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), () -> currentSkinnable.modifyGameSettings(), popup), new IconedMenuItem(SVG.SETTINGS, i18n("version.manage.manage"), () -> currentSkinnable.modifyGameSettings(), popup),
@@ -87,7 +87,7 @@ public class GameListItemSkin extends SkinBase<GameListItem> {
{ {
JFXButton btnLaunch = new JFXButton(); JFXButton btnLaunch = new JFXButton();
btnLaunch.setOnAction(e -> skinnable.launch()); btnLaunch.setOnAction(e -> skinnable.testGame());
btnLaunch.getStyleClass().add("toggle-icon4"); btnLaunch.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(btnLaunch, Pos.CENTER); BorderPane.setAlignment(btnLaunch, Pos.CENTER);
btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24)); btnLaunch.setGraphic(FXUtils.limitingSize(SVG.ROCKET_LAUNCH.createIcon(24), 24, 24));

View File

@@ -23,10 +23,7 @@ import javafx.stage.FileChooser;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount; import org.jackhuang.hmcl.auth.authlibinjector.AuthlibInjectorAccount;
import org.jackhuang.hmcl.download.game.GameAssetDownloadTask; import org.jackhuang.hmcl.download.game.GameAssetDownloadTask;
import org.jackhuang.hmcl.game.GameDirectoryType; import org.jackhuang.hmcl.game.*;
import org.jackhuang.hmcl.game.GameRepository;
import org.jackhuang.hmcl.game.HMCLGameRepository;
import org.jackhuang.hmcl.game.LauncherHelper;
import org.jackhuang.hmcl.mod.RemoteMod; import org.jackhuang.hmcl.mod.RemoteMod;
import org.jackhuang.hmcl.setting.*; import org.jackhuang.hmcl.setting.*;
import org.jackhuang.hmcl.task.*; import org.jackhuang.hmcl.task.*;
@@ -200,7 +197,8 @@ public final class Versions {
} }
} }
public static void generateLaunchScript(Profile profile, String id) { @SafeVarargs
public static void generateLaunchScript(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(profile, id)) if (!checkVersionForLaunching(profile, id))
return; return;
ensureSelectedAccount(account -> { ensureSelectedAccount(account -> {
@@ -219,18 +217,25 @@ public final class Versions {
: new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh")); : new FileChooser.ExtensionFilter(i18n("extension.sh"), "*.sh"));
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1")); chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter(i18n("extension.ps1"), "*.ps1"));
Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage())); Path file = FileUtils.toPath(chooser.showSaveDialog(Controllers.getStage()));
if (file != null) if (file != null) {
new LauncherHelper(profile, account, id).makeLaunchScript(file); LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
for (Consumer<LauncherHelper> injecter : injecters) {
injecter.accept(launcherHelper);
}
launcherHelper.makeLaunchScript(file);
}
}); });
} }
public static void launch(Profile profile, String id, Consumer<LauncherHelper> injecter) { @SafeVarargs
public static void launch(Profile profile, String id, Consumer<LauncherHelper>... injecters) {
if (!checkVersionForLaunching(profile, id)) if (!checkVersionForLaunching(profile, id))
return; return;
ensureSelectedAccount(account -> { ensureSelectedAccount(account -> {
LauncherHelper launcherHelper = new LauncherHelper(profile, account, id); LauncherHelper launcherHelper = new LauncherHelper(profile, account, id);
if (injecter != null) for (Consumer<LauncherHelper> injecter : injecters) {
injecter.accept(launcherHelper); injecter.accept(launcherHelper);
}
launcherHelper.launch(); launcherHelper.launch();
}); });
} }
@@ -239,6 +244,16 @@ public final class Versions {
launch(profile, id, LauncherHelper::setTestMode); launch(profile, id, LauncherHelper::setTestMode);
} }
public static void launchAndEnterWorld(Profile profile, String id, String worldFolderName) {
launch(profile, id, launcherHelper ->
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
}
public static void generateLaunchScriptForQuickEnterWorld(Profile profile, String id, String worldFolderName) {
generateLaunchScript(profile, id, launcherHelper ->
launcherHelper.setQuickPlayOption(new QuickPlayOption.SinglePlayer(worldFolderName)));
}
private static boolean checkVersionForLaunching(Profile profile, String id) { private static boolean checkVersionForLaunching(Profile profile, String id) {
if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id)) { if (id == null || !profile.getRepository().isLoaded() || !profile.getRepository().hasVersion(id)) {
JFXButton gotoDownload = new JFXButton(i18n("version.empty.launch.goto_download")); JFXButton gotoDownload = new JFXButton(i18n("version.empty.launch.goto_download"));

View File

@@ -22,6 +22,7 @@ import javafx.scene.control.Skin;
import javafx.stage.FileChooser; import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.game.WorldLockedException; import org.jackhuang.hmcl.game.WorldLockedException;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.Controllers; import org.jackhuang.hmcl.ui.Controllers;
@@ -39,11 +40,15 @@ public final class WorldListItem extends Control {
private final World world; private final World world;
private final Path backupsDir; private final Path backupsDir;
private final WorldListPage parent; private final WorldListPage parent;
private final Profile profile;
private final String id;
public WorldListItem(WorldListPage parent, World world, Path backupsDir) { public WorldListItem(WorldListPage parent, World world, Path backupsDir, Profile profile, String id) {
this.world = world; this.world = world;
this.backupsDir = backupsDir; this.backupsDir = backupsDir;
this.parent = parent; this.parent = parent;
this.profile = profile;
this.id = id;
} }
@Override @Override
@@ -91,6 +96,14 @@ public final class WorldListItem extends Control {
} }
public void showManagePage() { public void showManagePage() {
Controllers.navigate(new WorldManagePage(world, backupsDir)); Controllers.navigate(new WorldManagePage(world, backupsDir, profile, id));
}
public void launch() {
Versions.launchAndEnterWorld(profile, id, world.getFileName());
}
public void generateLaunchScript() {
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
} }
} }

View File

@@ -115,8 +115,19 @@ public final class WorldListItemSkin extends SkinBase<WorldListItem> {
WorldListItem item = getSkinnable(); WorldListItem item = getSkinnable();
World world = item.getWorld(); World world = item.getWorld();
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
IconedMenuItem launchItem = new IconedMenuItem(SVG.ROCKET_LAUNCH, i18n("version.launch_and_enter_world"), item::launch, popup);
launchItem.setDisable(world.isLocked());
popupMenu.getContent().add(launchItem);
popupMenu.getContent().addAll( popupMenu.getContent().addAll(
new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), item::showManagePage, popup)); new IconedMenuItem(SVG.SCRIPT, i18n("version.launch_script"), item::generateLaunchScript, popup),
new MenuSeparator()
);
}
popupMenu.getContent().add(new IconedMenuItem(SVG.SETTINGS, i18n("world.manage.button"), item::showManagePage, popup));
if (ChunkBaseApp.isSupported(world)) { if (ChunkBaseApp.isSupported(world)) {
popupMenu.getContent().addAll( popupMenu.getContent().addAll(

View File

@@ -61,7 +61,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
if (worlds != null) if (worlds != null)
itemsProperty().setAll(worlds.stream() itemsProperty().setAll(worlds.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion)) .filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(world -> new WorldListItem(this, world, backupsDir)).toList()); .map(world -> new WorldListItem(this, world, backupsDir, profile, id)).toList());
}); });
} }
@@ -100,7 +100,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
if (exception == null) { if (exception == null) {
itemsProperty().setAll(result.stream() itemsProperty().setAll(result.stream()
.filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion)) .filter(world -> isShowAll() || world.getGameVersion() == null || world.getGameVersion().equals(gameVersion))
.map(world -> new WorldListItem(this, world, backupsDir)) .map(world -> new WorldListItem(this, world, backupsDir, profile, id))
.collect(Collectors.toList())); .collect(Collectors.toList()));
} else { } else {
LOG.warning("Failed to load world list page", exception); LOG.warning("Failed to load world list page", exception);
@@ -131,7 +131,7 @@ public final class WorldListPage extends ListPageBase<WorldListItem> implements
Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> { Controllers.prompt(i18n("world.name.enter"), (name, resolve, reject) -> {
Task.runAsync(() -> world.install(savesDir, name)) Task.runAsync(() -> world.install(savesDir, name))
.whenComplete(Schedulers.javafx(), () -> { .whenComplete(Schedulers.javafx(), () -> {
itemsProperty().add(new WorldListItem(this, new World(savesDir.resolve(name)), backupsDir)); itemsProperty().add(new WorldListItem(this, new World(savesDir.resolve(name)), backupsDir, profile, id));
resolve.run(); resolve.run();
}, e -> { }, e -> {
if (e instanceof FileAlreadyExistsException) if (e instanceof FileAlreadyExistsException)

View File

@@ -26,6 +26,7 @@ import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Priority; import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox; import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.game.World; import org.jackhuang.hmcl.game.World;
import org.jackhuang.hmcl.setting.Profile;
import org.jackhuang.hmcl.ui.FXUtils; import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG; import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.animation.TransitionPane;
@@ -49,6 +50,8 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private final ObjectProperty<State> state; private final ObjectProperty<State> state;
private final World world; private final World world;
private final Path backupsDir; private final Path backupsDir;
private final Profile profile;
private final String id;
private final TabHeader header; private final TabHeader header;
private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage"); private final TabHeader.Tab<WorldInfoPage> worldInfoTab = new TabHeader.Tab<>("worldInfoPage");
@@ -59,10 +62,13 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
private FileChannel sessionLockChannel; private FileChannel sessionLockChannel;
public WorldManagePage(World world, Path backupsDir) { public WorldManagePage(World world, Path backupsDir, Profile profile, String id) {
this.world = world; this.world = world;
this.backupsDir = backupsDir; this.backupsDir = backupsDir;
this.profile = profile;
this.id = id;
this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this)); this.worldInfoTab.setNodeSupplier(() -> new WorldInfoPage(this));
this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this)); this.worldBackupsTab.setNodeSupplier(() -> new WorldBackupsPage(this));
this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this)); this.datapackTab.setNodeSupplier(() -> new DatapackListPage(this));
@@ -71,6 +77,13 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab); this.header = new TabHeader(transitionPane, worldInfoTab, worldBackupsTab);
header.select(worldInfoTab); header.select(worldInfoTab);
// Does it need to be done in the background?
try {
sessionLockChannel = world.lock();
LOG.info("Acquired lock on world " + world.getFileName());
} catch (IOException ignored) {
}
setCenter(transitionPane); setCenter(transitionPane);
BorderPane left = new BorderPane(); BorderPane left = new BorderPane();
@@ -91,6 +104,12 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
left.setTop(sideBar); left.setTop(sideBar);
AdvancedListBox toolbar = new AdvancedListBox(); AdvancedListBox toolbar = new AdvancedListBox();
if (world.getGameVersion() != null && world.getGameVersion().isAtLeast("1.20", "23w14a")) {
toolbar.addNavigationDrawerItem(i18n("version.launch"), SVG.ROCKET_LAUNCH, this::launch, null);
toolbar.addNavigationDrawerItem(i18n("version.launch_script"), SVG.SCRIPT, this::generateLaunchScript, null);
}
if (ChunkBaseApp.isSupported(world)) { if (ChunkBaseApp.isSupported(world)) {
PopupMenu popupMenu = new PopupMenu(); PopupMenu popupMenu = new PopupMenu();
JFXPopup popup = new JFXPopup(popupMenu); JFXPopup popup = new JFXPopup(popupMenu);
@@ -117,12 +136,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0)); BorderPane.setMargin(toolbar, new Insets(0, 0, 12, 0));
left.setBottom(toolbar); left.setBottom(toolbar);
// Does it need to be done in the background? this.addEventHandler(Navigator.NavigationEvent.EXITED, this::onExited);
try {
sessionLockChannel = world.lock();
LOG.info("Acquired lock on world " + world.getFileName());
} catch (IOException ignored) {
}
} }
@Override @Override
@@ -142,14 +156,7 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
return sessionLockChannel == null; return sessionLockChannel == null;
} }
@Override public void onExited(Navigator.NavigationEvent event) {
public boolean back() {
closePage();
return true;
}
@Override
public void closePage() {
if (sessionLockChannel != null) { if (sessionLockChannel != null) {
try { try {
sessionLockChannel.close(); sessionLockChannel.close();
@@ -161,4 +168,13 @@ public final class WorldManagePage extends DecoratorAnimatedPage implements Deco
sessionLockChannel = null; sessionLockChannel = null;
} }
} }
public void launch() {
fireEvent(new PageCloseEvent());
Versions.launchAndEnterWorld(profile, id, world.getFileName());
}
public void generateLaunchScript() {
Versions.generateLaunchScriptForQuickEnterWorld(profile, id, world.getFileName());
}
} }

View File

@@ -1560,6 +1560,7 @@ version.game.support_status.unsupported=Unsupported
version.game.support_status.untested=Untested version.game.support_status.untested=Untested
version.game.type=Type version.game.type=Type
version.launch=Launch Game version.launch=Launch Game
version.launch_and_enter_world=Play World
version.launch.empty=Start Game version.launch.empty=Start Game
version.launch.empty.installing=Installing Game version.launch.empty.installing=Installing Game
version.launch.empty.tooltip=Install and launch the latest official release version.launch.empty.tooltip=Install and launch the latest official release

View File

@@ -1344,6 +1344,7 @@ version.game.support_status.unsupported=不支援
version.game.support_status.untested=未經測試 version.game.support_status.untested=未經測試
version.game.type=版本類型 version.game.type=版本類型
version.launch=啟動遊戲 version.launch=啟動遊戲
version.launch_and_enter_world=進入世界
version.launch.empty=開始遊戲 version.launch.empty=開始遊戲
version.launch.empty.installing=安裝遊戲 version.launch.empty.installing=安裝遊戲
version.launch.empty.tooltip=安裝並啟動最新正式版遊戲 version.launch.empty.tooltip=安裝並啟動最新正式版遊戲

View File

@@ -1354,6 +1354,7 @@ version.game.support_status.unsupported=不支持
version.game.support_status.untested=未经测试 version.game.support_status.untested=未经测试
version.game.type=版本类型 version.game.type=版本类型
version.launch=启动游戏 version.launch=启动游戏
version.launch_and_enter_world=进入世界
version.launch.empty=开始游戏 version.launch.empty=开始游戏
version.launch.empty.installing=安装游戏 version.launch.empty.installing=安装游戏
version.launch.empty.tooltip=安装并启动最新正式版游戏 version.launch.empty.tooltip=安装并启动最新正式版游戏

View File

@@ -47,7 +47,7 @@ public class LaunchOptions implements Serializable {
private Integer width; private Integer width;
private Integer height; private Integer height;
private boolean fullscreen; private boolean fullscreen;
private String serverIp; private QuickPlayOption quickPlayOption;
private String wrapper; private String wrapper;
private Proxy.Type proxyType; private Proxy.Type proxyType;
private String proxyHost; private String proxyHost;
@@ -181,11 +181,11 @@ public class LaunchOptions implements Serializable {
return fullscreen; return fullscreen;
} }
/** /// The quick play option.
* The server ip that will connect to when enter game main menu. ///
*/ /// @see <a href="https://minecraft.wiki/w/Quick_Play">Quick Play - Minecraft Wiki</a>
public String getServerIp() { public QuickPlayOption getQuickPlayOption() {
return serverIp; return quickPlayOption;
} }
/** /**
@@ -412,8 +412,8 @@ public class LaunchOptions implements Serializable {
return this; return this;
} }
public Builder setServerIp(String serverIp) { public Builder setQuickPlayOption(QuickPlayOption quickPlayOption) {
options.serverIp = serverIp; options.quickPlayOption = quickPlayOption;
return this; return this;
} }

View File

@@ -0,0 +1,32 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.game;
/// The quick play option.
///
/// @see <a href="https://minecraft.wiki/w/Quick_Play">Quick Play - Minecraft Wiki</a>
public sealed interface QuickPlayOption {
record SinglePlayer(String worldFolderName) implements QuickPlayOption {
}
record MultiPlayer(String serverIP) implements QuickPlayOption {
}
record Realm(String realmID) implements QuickPlayOption {
}
}

View File

@@ -304,23 +304,31 @@ public class DefaultLauncher extends Launcher {
if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.getGame() != null && !argumentsFromAuthInfo.getGame().isEmpty()) if (argumentsFromAuthInfo != null && argumentsFromAuthInfo.getGame() != null && !argumentsFromAuthInfo.getGame().isEmpty())
res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getGame(), configuration, features)); res.addAll(Arguments.parseArguments(argumentsFromAuthInfo.getGame(), configuration, features));
if (StringUtils.isNotBlank(options.getServerIp())) { if (options.getQuickPlayOption() instanceof QuickPlayOption.MultiPlayer multiPlayer) {
String address = options.getServerIp(); String address = multiPlayer.serverIP();
try { try {
ServerAddress parsed = ServerAddress.parse(address); ServerAddress parsed = ServerAddress.parse(address);
if (GameVersionNumber.asGameVersion(gameVersion).compareTo("1.20") < 0) { if (GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) {
res.add("--quickPlayMultiplayer");
res.add(parsed.getPort() >= 0 ? address : parsed.getHost() + ":25565");
} else {
res.add("--server"); res.add("--server");
res.add(parsed.getHost()); res.add(parsed.getHost());
res.add("--port"); res.add("--port");
res.add(parsed.getPort() >= 0 ? String.valueOf(parsed.getPort()) : "25565"); res.add(parsed.getPort() >= 0 ? String.valueOf(parsed.getPort()) : "25565");
} else {
res.add("--quickPlayMultiplayer");
res.add(parsed.getPort() < 0 ? address + ":25565" : address);
} }
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
LOG.warning("Invalid server address: " + address, e); LOG.warning("Invalid server address: " + address, e);
} }
} else if (options.getQuickPlayOption() instanceof QuickPlayOption.SinglePlayer singlePlayer
&& GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) {
res.add("--quickPlaySingleplayer");
res.add(singlePlayer.worldFolderName());
} else if (options.getQuickPlayOption() instanceof QuickPlayOption.Realm realm
&& GameVersionNumber.asGameVersion(gameVersion).isAtLeast("1.20", "23w14a")) {
res.add("--quickPlayRealms");
res.add(realm.realmID());
} }
if (options.isFullscreen()) if (options.isFullscreen())