feat(multiplayer): detect game port when creating multiplayer room. Update cato to 1.0.8.

This commit is contained in:
huanghongxun
2021-09-26 00:02:48 +08:00
parent 7fd8e0721f
commit 54954f276b
15 changed files with 523 additions and 78 deletions

View File

@@ -28,4 +28,7 @@ public interface DialogAware {
default void onDialogShown() {
}
default void onDialogClosed() {
}
}

View File

@@ -0,0 +1,118 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import com.jfoenix.controls.JFXProgressBar;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class DialogPane extends JFXDialogLayout {
private final StringProperty title = new SimpleStringProperty();
private final BooleanProperty valid = new SimpleBooleanProperty();
private final SpinnerPane acceptPane = new SpinnerPane();
private final Label warningLabel = new Label();
private final JFXProgressBar progressBar = new JFXProgressBar();
public DialogPane() {
Label titleLabel = new Label();
titleLabel.textProperty().bind(title);
setHeading(titleLabel);
getChildren().add(progressBar);
progressBar.setVisible(false);
StackPane.setMargin(progressBar, new Insets(-24.0D, -24.0D, -16.0D, -24.0D));
StackPane.setAlignment(progressBar, Pos.TOP_CENTER);
progressBar.setMaxWidth(Double.MAX_VALUE);
JFXButton acceptButton = new JFXButton(i18n("button.ok"));
acceptButton.setOnAction(e -> onAccept());
acceptButton.disableProperty().bind(valid.not());
acceptButton.getStyleClass().add("dialog-accept");
acceptPane.getStyleClass().add("small-spinner-pane");
acceptPane.setContent(acceptButton);
JFXButton cancelButton = new JFXButton(i18n("button.cancel"));
cancelButton.setOnAction(e -> onCancel());
cancelButton.getStyleClass().add("dialog-cancel");
onEscPressed(this, cancelButton::fire);
setActions(warningLabel, acceptPane, cancelButton);
}
protected JFXProgressBar getProgressBar() {
return progressBar;
}
public String getTitle() {
return title.get();
}
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
public boolean isValid() {
return valid.get();
}
public BooleanProperty validProperty() {
return valid;
}
public void setValid(boolean valid) {
this.valid.set(valid);
}
protected void onCancel() {
fireEvent(new DialogCloseEvent());
}
protected void onAccept() {
fireEvent(new DialogCloseEvent());
}
protected void setLoading() {
acceptPane.showSpinner();
warningLabel.setText("");
}
protected void onSuccess() {
acceptPane.hideSpinner();
fireEvent(new DialogCloseEvent());
}
protected void onFailure(String msg) {
acceptPane.hideSpinner();
warningLabel.setText(msg);
}
}

View File

@@ -50,6 +50,7 @@ public class Navigator extends TransitionPane {
getChildren().setAll(init);
fireEvent(new NavigationEvent(this, init, Navigation.NavigationDirection.START, NavigationEvent.NAVIGATED));
if (init instanceof PageAware) ((PageAware) init).onPageShown();
initialized = true;
}
@@ -78,6 +79,7 @@ public class Navigator extends TransitionPane {
NavigationEvent navigated = new NavigationEvent(this, node, Navigation.NavigationDirection.NEXT, NavigationEvent.NAVIGATED);
node.fireEvent(navigated);
if (node instanceof PageAware) ((PageAware) node).onPageShown();
EventHandler<PageCloseEvent> handler = event -> close(node);
node.getProperties().put(PROPERTY_DIALOG_CLOSE_HANDLER, handler);
@@ -108,7 +110,9 @@ public class Navigator extends TransitionPane {
Logging.LOG.info("Closed page " + from);
stack.pop();
Node poppedNode = stack.pop();
if (poppedNode instanceof PageAware) ((PageAware) poppedNode).onPageHidden();
backable.set(canGoBack());
Node node = stack.peek();
@@ -202,6 +206,7 @@ public class Navigator extends TransitionPane {
};
public static class NavigationEvent extends Event {
public static final EventType<NavigationEvent> EXITED = new EventType<>("EXITED");
public static final EventType<NavigationEvent> NAVIGATED = new EventType<>("NAVIGATED");
public static final EventType<NavigationEvent> NAVIGATING = new EventType<>("NAVIGATING");

View File

@@ -0,0 +1,26 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.construct;
public interface PageAware {
default void onPageShown() {
}
default void onPageHidden() {
}
}

View File

@@ -17,52 +17,44 @@
*/
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXCheckBox;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.scene.control.Label;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.FutureCallback;
import org.jackhuang.hmcl.util.StringUtils;
import static org.jackhuang.hmcl.ui.FXUtils.onEscPressed;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
public class PromptDialogPane extends StackPane {
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
public class PromptDialogPane extends DialogPane {
private final CompletableFuture<List<Builder.Question<?>>> future = new CompletableFuture<>();
@FXML
private JFXButton acceptButton;
@FXML
private JFXButton cancelButton;
@FXML
private VBox vbox;
@FXML
private Label title;
@FXML
private Label lblCreationWarning;
@FXML
private SpinnerPane acceptPane;
private final Builder builder;
public PromptDialogPane(Builder builder) {
FXUtils.loadFXML(this, "/assets/fxml/input-dialog.fxml");
this.title.setText(builder.title);
this.builder = builder;
setTitle(builder.title);
GridPane body = new GridPane();
body.setVgap(8);
body.setHgap(16);
body.getColumnConstraints().setAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
setBody(body);
List<BooleanBinding> bindings = new ArrayList<>();
int rowIndex = 0;
for (Builder.Question<?> question : builder.questions) {
if (question instanceof Builder.StringQuestion) {
Builder.StringQuestion stringQuestion = (Builder.StringQuestion) question;
@@ -76,62 +68,58 @@ public class PromptDialogPane extends StackPane {
bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty()));
if (StringUtils.isNotBlank(question.question)) {
vbox.getChildren().add(new Label(question.question));
body.addRow(rowIndex++, new Label(question.question), textField);
} else {
GridPane.setColumnSpan(textField, 2);
body.addRow(rowIndex++, textField);
}
VBox.setMargin(textField, new Insets(0, 0, 20, 0));
vbox.getChildren().add(textField);
GridPane.setMargin(textField, new Insets(0, 0, 20, 0));
} else if (question instanceof Builder.BooleanQuestion) {
HBox hBox = new HBox();
GridPane.setColumnSpan(hBox, 2);
JFXCheckBox checkBox = new JFXCheckBox();
hBox.getChildren().setAll(checkBox);
HBox.setMargin(checkBox, new Insets(0, 0, 0, -10));
checkBox.setSelected(((Builder.BooleanQuestion) question).value);
checkBox.selectedProperty().addListener((a, b, newValue) -> ((Builder.BooleanQuestion) question).value = newValue);
checkBox.setText(question.question);
vbox.getChildren().add(hBox);
body.addRow(rowIndex++, hBox);
} else if (question instanceof Builder.CandidatesQuestion) {
HBox hBox = new HBox();
JFXComboBox<String> comboBox = new JFXComboBox<>();
hBox.getChildren().setAll(comboBox);
comboBox.getItems().setAll(((Builder.CandidatesQuestion) question).candidates);
comboBox.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) ->
((Builder.CandidatesQuestion) question).value = newValue.intValue());
comboBox.getSelectionModel().select(0);
if (StringUtils.isNotBlank(question.question)) {
vbox.getChildren().add(new Label(question.question));
body.addRow(rowIndex++, new Label(question.question), comboBox);
} else {
GridPane.setColumnSpan(comboBox, 2);
body.addRow(rowIndex++, comboBox);
}
vbox.getChildren().add(hBox);
} else if (question instanceof Builder.HintQuestion) {
HintPane pane = new HintPane();
GridPane.setColumnSpan(pane, 2);
pane.setText(question.question);
vbox.getChildren().add(pane);
body.addRow(rowIndex++, pane);
}
}
cancelButton.setOnMouseClicked(e -> fireEvent(new DialogCloseEvent()));
acceptButton.disableProperty().bind(Bindings.createBooleanBinding(
() -> bindings.stream().map(BooleanBinding::get).anyMatch(x -> !x),
validProperty().bind(Bindings.createBooleanBinding(
() -> bindings.stream().allMatch(BooleanBinding::get),
bindings.toArray(new BooleanBinding[0])
));
}
acceptButton.setOnMouseClicked(e -> {
acceptPane.showSpinner();
@Override
protected void onAccept() {
setLoading();
builder.callback.call(builder.questions, () -> {
future.complete(builder.questions);
runInFX(() -> {
acceptPane.hideSpinner();
fireEvent(new DialogCloseEvent());
});
}, msg -> {
runInFX(() -> {
acceptPane.hideSpinner();
lblCreationWarning.setText(msg);
});
});
builder.callback.call(builder.questions, () -> {
future.complete(builder.questions);
runInFX(this::onSuccess);
}, msg -> {
runInFX(() -> onFailure(msg));
});
onEscPressed(this, cancelButton::fire);
}
public CompletableFuture<List<Builder.Question<?>>> getCompletableFuture() {

View File

@@ -360,6 +360,10 @@ public class DecoratorController {
if (dialog != null) {
dialogPane.pop(node);
if (node instanceof DialogAware) {
((DialogAware) node).onDialogClosed();
}
if (dialogPane.getChildren().isEmpty()) {
dialog.close();
dialog = null;

View File

@@ -0,0 +1,119 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.DialogAware;
import org.jackhuang.hmcl.ui.construct.DialogPane;
import org.jackhuang.hmcl.ui.construct.HintPane;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.util.FutureCallback;
import java.util.Objects;
import java.util.Optional;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAware {
private final FutureCallback<LocalServerDetector.PingResponse> callback;
private final LocalServerDetector lanServerDetectorThread;
private LocalServerDetector.PingResponse server;
CreateMultiplayerRoomDialog(FutureCallback<LocalServerDetector.PingResponse> callback) {
this.callback = callback;
setTitle(i18n("multiplayer.session.create"));
GridPane body = new GridPane();
body.setMaxWidth(500);
body.getColumnConstraints().addAll(new ColumnConstraints(), FXUtils.getColumnHgrowing());
body.setVgap(8);
body.setHgap(16);
body.setDisable(true);
setBody(body);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
hintPane.setText(i18n("multiplayer.session.create.hint"));
GridPane.setColumnSpan(hintPane, 2);
body.addRow(0, hintPane);
Label nameField = new Label();
nameField.setText(Optional.ofNullable(Accounts.getSelectedAccount())
.map(Account::getUsername)
.map(username -> i18n("multiplayer.session.name.format", username))
.orElse(""));
body.addRow(1, new Label(i18n("multiplayer.session.create.name")), nameField);
Label portLabel = new Label(i18n("multiplayer.nat.testing"));
portLabel.setText(i18n("multiplayer.nat.testing"));
body.addRow(2, new Label(i18n("multiplayer.session.create.port")), portLabel);
setValid(false);
lanServerDetectorThread = new LocalServerDetector(3);
lanServerDetectorThread.onDetectedLanServer().register(event -> {
runInFX(() -> {
if (event.getLanServer().isValid()) {
nameField.setText(event.getLanServer().getMotd());
portLabel.setText(event.getLanServer().getAd().toString());
setValid(true);
} else {
nameField.setText("");
portLabel.setText("");
onFailure(i18n("multiplayer.session.create.port.error"));
}
server = event.getLanServer();
body.setDisable(false);
getProgressBar().setVisible(false);
});
});
}
@Override
protected void onAccept() {
setLoading();
callback.call(Objects.requireNonNull(server), () -> {
runInFX(this::onSuccess);
}, msg -> {
runInFX(() -> onFailure(msg));
});
}
@Override
public void onDialogShown() {
getProgressBar().setVisible(true);
getProgressBar().setProgress(ProgressIndicator.INDETERMINATE_PROGRESS);
lanServerDetectorThread.start();
}
@Override
public void onDialogClosed() {
lanServerDetectorThread.interrupt();
}
}

View File

@@ -21,7 +21,6 @@ import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
@@ -44,9 +43,11 @@ public class LocalServerBroadcaster implements Runnable {
@Override
public void run() {
DatagramSocket socket;
InetAddress broadcastAddress;
try {
socket = new DatagramSocket();
} catch (SocketException e) {
broadcastAddress = InetAddress.getByName("224.0.2.60");
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create datagram socket", e);
return;
}
@@ -54,8 +55,9 @@ public class LocalServerBroadcaster implements Runnable {
while (session.isRunning()) {
try {
byte[] data = String.format("[MOTD]%s[/MOTD][AD]%d[/AD]", i18n("multiplayer.session.name.motd", session.getName()), port).getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(data, 0, data.length, InetAddress.getByName("224.0.2.60"), 4445);
DatagramPacket packet = new DatagramPacket(data, 0, data.length, broadcastAddress, 4445);
socket.send(packet);
LOG.fine("Broadcast server 127.0.0.1:" + port);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to send motd packet", e);
}
@@ -66,5 +68,7 @@ public class LocalServerBroadcaster implements Runnable {
return;
}
}
socket.close();
}
}

View File

@@ -0,0 +1,141 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.jackhuang.hmcl.ui.multiplayer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import static org.jackhuang.hmcl.util.Logging.LOG;
public class LocalServerDetector extends Thread {
private final EventManager<DetectedLanServerEvent> onDetectedLanServer = new EventManager<>();
private final int retry;
public LocalServerDetector(int retry) {
this.retry = retry;
setName("LocalServerDetector");
setDaemon(true);
}
public EventManager<DetectedLanServerEvent> onDetectedLanServer() {
return onDetectedLanServer;
}
@Override
public void run() {
MulticastSocket socket;
InetAddress broadcastAddress;
try {
socket = new MulticastSocket(4445);
socket.setSoTimeout(5000);
socket.joinGroup(broadcastAddress = InetAddress.getByName("224.0.2.60"));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to create datagram socket", e);
return;
}
byte[] buf = new byte[1024];
int tried = 0;
while (!isInterrupted()) {
DatagramPacket packet = new DatagramPacket(buf, 1024);
try {
socket.receive(packet);
} catch (SocketTimeoutException e) {
if (tried++ > retry) {
onDetectedLanServer.fireEvent(new DetectedLanServerEvent(this, null));
break;
}
continue;
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to detect lan server", e);
break;
}
String response = new String(packet.getData(), packet.getOffset(), packet.getLength(), StandardCharsets.UTF_8);
LOG.fine("Local server broadcast message: " + response);
onDetectedLanServer.fireEvent(new DetectedLanServerEvent(this, PingResponse.parsePingResponse(response)));
break;
}
try {
socket.leaveGroup(broadcastAddress);
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to leave multicast listening group", e);
}
socket.close();
}
public static class DetectedLanServerEvent extends Event {
private final PingResponse lanServer;
public DetectedLanServerEvent(Object source, PingResponse lanServer) {
super(source);
this.lanServer = lanServer;
}
public PingResponse getLanServer() {
return lanServer;
}
}
public static class PingResponse {
private final String motd;
private final Integer ad;
public PingResponse(String motd, Integer ad) {
this.motd = motd;
this.ad = ad;
}
public String getMotd() {
return motd;
}
public Integer getAd() {
return ad;
}
public boolean isValid() {
return ad != null;
}
public static PingResponse parsePingResponse(String message) {
return new PingResponse(
StringUtils.substringBefore(
StringUtils.substringAfter(message, "[MOTD]"),
"[/MOTD]"),
Lang.toIntOrNull(StringUtils.substringBefore(
StringUtils.substringAfter(message, "[AD]"),
"[/AD]"))
);
}
}
}

View File

@@ -21,7 +21,6 @@ import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.Metadata;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.game.Artifact;
import org.jackhuang.hmcl.launch.StreamPump;
import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
@@ -40,7 +39,10 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.util.*;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -51,17 +53,15 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
*/
public final class MultiplayerManager {
private static final String CATO_DOWNLOAD_URL = "https://files.huangyuhui.net/maven/";
private static final String CATO_VERSION = "2021-09-25";
private static final Artifact CATO_ARTIFACT = new Artifact("cato", "cato", CATO_VERSION,
OperatingSystem.CURRENT_OS.getCheckedName() + "-" + Architecture.CURRENT.name().toLowerCase(Locale.ROOT),
OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "exe" : null);
private static final String CATO_VERSION = "1.0.8";
private static final String CATO_PATH = getCatoPath();
private MultiplayerManager() {
}
public static Task<Void> downloadCato() {
return new FileDownloadTask(
NetworkUtils.toURL(CATO_DOWNLOAD_URL + CATO_ARTIFACT.getPath()),
NetworkUtils.toURL(CATO_DOWNLOAD_URL + CATO_PATH),
getCatoExecutable().toFile()
).thenRunAsync(() -> {
if (OperatingSystem.CURRENT_OS == OperatingSystem.LINUX || OperatingSystem.CURRENT_OS == OperatingSystem.OSX) {
@@ -73,7 +73,7 @@ public final class MultiplayerManager {
}
public static Path getCatoExecutable() {
return CATO_ARTIFACT.getPath(Metadata.HMCL_DIRECTORY.resolve("libraries"));
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH);
}
public static CatoSession joinSession(String version, String sessionName, String peer, int remotePort, int localPort) throws IOException, IncompatibleCatoVersionException {
@@ -123,6 +123,33 @@ public final class MultiplayerManager {
}
}
public static String getCatoPath() {
switch (OperatingSystem.CURRENT_OS) {
case WINDOWS:
if (Architecture.CURRENT == Architecture.X86_64) {
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-windows-amd64.exe";
} else {
return "";
}
case OSX:
if (Architecture.CURRENT == Architecture.X86_64) {
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-darwin-amd64";
} else {
return "";
}
case LINUX:
if (Architecture.CURRENT == Architecture.X86_64) {
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-linux-amd64";
} else if (Architecture.CURRENT == Architecture.ARM || Architecture.CURRENT == Architecture.ARM64) {
return "cato/cato/" + MultiplayerManager.CATO_VERSION + "/cato-linux-arm7";
} else {
return "";
}
default:
return "";
}
}
public static class CatoSession extends ManagedProcess {
private final EventManager<CatoExitEvent> onExit = new EventManager<>();
private final EventManager<CatoIdEvent> onIdGenerated = new EventManager<>();

View File

@@ -29,11 +29,9 @@ import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.task.TaskExecutor;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.construct.MessageDialogPane;
import org.jackhuang.hmcl.ui.construct.NumberValidator;
import org.jackhuang.hmcl.ui.construct.PromptDialogPane;
import org.jackhuang.hmcl.ui.construct.RequiredValidator;
import org.jackhuang.hmcl.ui.construct.*;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
import org.jackhuang.hmcl.util.StringUtils;
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
@@ -43,7 +41,7 @@ import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class MultiplayerPage extends Control implements DecoratorPage {
public class MultiplayerPage extends Control implements DecoratorPage, PageAware {
private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("multiplayer"), -1));
private final ObjectProperty<MultiplayerManager.State> multiplayerState = new SimpleObjectProperty<>(MultiplayerManager.State.DISCONNECTED);
@@ -58,6 +56,10 @@ public class MultiplayerPage extends Control implements DecoratorPage {
public MultiplayerPage() {
testNAT();
}
@Override
public void onPageShown() {
downloadCatoIfNecessary();
}
@@ -124,6 +126,12 @@ public class MultiplayerPage extends Control implements DecoratorPage {
}
private void downloadCatoIfNecessary() {
if (StringUtils.isBlank(MultiplayerManager.getCatoPath())) {
Controllers.dialog(i18n("multiplayer.download."), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
fireEvent(new PageCloseEvent());
return;
}
if (!MultiplayerManager.getCatoExecutable().toFile().exists()) {
setDisabled(true);
TaskExecutor executor = MultiplayerManager.downloadCato()
@@ -134,6 +142,7 @@ public class MultiplayerPage extends Control implements DecoratorPage {
Controllers.showToast(i18n("message.cancelled"));
} else {
Controllers.dialog(DownloadProviders.localizeErrorMessage(exception), i18n("install.failed.downloading"), MessageDialogPane.MessageType.ERROR);
fireEvent(new PageCloseEvent());
}
} else {
Controllers.showToast(i18n("multiplayer.download.success"));
@@ -159,10 +168,10 @@ public class MultiplayerPage extends Control implements DecoratorPage {
throw new IllegalStateException("CatoSession already ready");
}
Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.create"), (result, resolve, reject) -> {
int port = Integer.parseInt(((PromptDialogPane.Builder.StringQuestion) result.get(2)).getValue());
Controllers.dialog(new CreateMultiplayerRoomDialog((result, resolve, reject) -> {
int port = result.getAd();
try {
initCatoSession(MultiplayerManager.createSession(((PromptDialogPane.Builder.StringQuestion) result.get(1)).getValue(), port));
initCatoSession(MultiplayerManager.createSession(result.getMotd(), port));
} catch (Exception e) {
LOG.log(Level.WARNING, "Failed to create session", e);
reject.accept(i18n("multiplayer.session.create.error"));
@@ -172,10 +181,7 @@ public class MultiplayerPage extends Control implements DecoratorPage {
this.port.set(port);
setMultiplayerState(MultiplayerManager.State.CONNECTING);
resolve.run();
})
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("multiplayer.session.create.hint")))
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.create.name"), "", new RequiredValidator()))
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.create.port"), "", new NumberValidator())));
}));
}
public void joinRoom() {

View File

@@ -165,7 +165,8 @@ public final class Versions {
}
}).start();
})
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("version.manage.duplicate.confirm"), version,
.addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("version.manage.duplicate.confirm")))
.addQuestion(new PromptDialogPane.Builder.StringQuestion(null, version,
new Validator(i18n("install.new_game.already_exists"), newVersionName -> !profile.getRepository().hasVersion(newVersionName))))
.addQuestion(new PromptDialogPane.Builder.BooleanQuestion(i18n("version.manage.duplicate.duplicate_save"), false)));
}

View File

@@ -584,8 +584,9 @@ mods.url=Official Page
multiplayer=Multiplayer
multiplayer.download=Downloading dependencies for multiplayer
multiplayer.download.success=Dependencies initialization succeeded
multiplayer.download.failed=Failed to initialize multiplayer, some files cannot be downloaded
multiplayer.download.success=Dependencies initialization succeeded
multiplayer.download.unsupported=Current operating system or architecure is unsupported.
multiplayer.exit.after_ready=Multiplayer session broken. cato exitcode %d
multiplayer.exit.before_ready=Multiplayer session failed to create. cato exitcode %d
multiplayer.exit.timeout=Failed to connect to multiplayer server.

View File

@@ -584,8 +584,9 @@ mods.url=官方頁面
multiplayer=多人聯機
multiplayer.download=正在下載相依元件
multiplayer.download.success=多人聯機初始化完成
multiplayer.download.failed=初始化失敗,部分文件未能完成下載
multiplayer.download.success=多人聯機初始化完成
multiplayer.download.unsupported=多人聯機依賴不支持當前系統或平台
multiplayer.exit.after_ready=多人聯機會話意外退出,退出碼 %d
multiplayer.exit.before_ready=多人聯機房間創建失敗cato 退出碼 %d
multiplayer.exit.timeout=無法連接多人聯機服務

View File

@@ -584,8 +584,9 @@ mods.url=官方页面
multiplayer=多人联机
multiplayer.download=正在下载依赖
multiplayer.download.success=多人联机初始化完成
multiplayer.download.failed=初始化失败,部分文件未能完成下载
multiplayer.download.success=多人联机初始化完成
multiplayer.download.unsupported=多人联机依赖不支持当前系统或平台
multiplayer.exit.after_ready=多人联机会话意外退出,退出码 %d
multiplayer.exit.before_ready=多人联机房间创建失败cato 退出码 %d
multiplayer.exit.timeout=无法连接多人联机服务