feat(multiplayer): detect game port when creating multiplayer room. Update cato to 1.0.8.
This commit is contained in:
@@ -28,4 +28,7 @@ public interface DialogAware {
|
||||
default void onDialogShown() {
|
||||
}
|
||||
|
||||
default void onDialogClosed() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]"))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<>();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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=無法連接多人聯機服務
|
||||
|
||||
@@ -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=无法连接多人联机服务
|
||||
|
||||
Reference in New Issue
Block a user