From eedcff785e96bfffc345d0787f2ac216bfb20953 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Thu, 7 Oct 2021 22:39:02 +0800 Subject: [PATCH] feat(multiplayer): an option for auto allowing all join requests. Closes #1093. --- .../hmcl/ui/construct/PromptDialogPane.java | 30 +++++++++++----- .../CreateMultiplayerRoomDialog.java | 35 +++++++++++++++++-- .../ui/multiplayer/MultiplayerManager.java | 25 +++++++++++-- .../hmcl/ui/multiplayer/MultiplayerPage.java | 17 +++++++-- .../ui/multiplayer/MultiplayerServer.java | 9 +++-- .../resources/assets/lang/I18N.properties | 3 ++ .../resources/assets/lang/I18N_zh.properties | 3 ++ .../assets/lang/I18N_zh_CN.properties | 3 ++ .../java/org/jackhuang/hmcl/util/Lang.java | 16 +++++++++ 9 files changed, 122 insertions(+), 19 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PromptDialogPane.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PromptDialogPane.java index c9913f6c8..de6ec52cc 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PromptDialogPane.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/PromptDialogPane.java @@ -23,6 +23,8 @@ import com.jfoenix.controls.JFXTextField; import com.jfoenix.validation.base.ValidatorBase; import javafx.beans.binding.Bindings; import javafx.beans.binding.BooleanBinding; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.geometry.Insets; import javafx.scene.control.Label; import javafx.scene.layout.ColumnConstraints; @@ -67,8 +69,8 @@ public class PromptDialogPane extends DialogPane { } bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty())); - if (StringUtils.isNotBlank(question.question)) { - body.addRow(rowIndex++, new Label(question.question), textField); + if (StringUtils.isNotBlank(question.question.get())) { + body.addRow(rowIndex++, new Label(question.question.get()), textField); } else { GridPane.setColumnSpan(textField, 2); body.addRow(rowIndex++, textField); @@ -82,7 +84,7 @@ public class PromptDialogPane extends DialogPane { 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); + checkBox.setText(question.question.get()); body.addRow(rowIndex++, hBox); } else if (question instanceof Builder.CandidatesQuestion) { JFXComboBox comboBox = new JFXComboBox<>(); @@ -90,8 +92,8 @@ public class PromptDialogPane extends DialogPane { comboBox.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> ((Builder.CandidatesQuestion) question).value = newValue.intValue()); comboBox.getSelectionModel().select(0); - if (StringUtils.isNotBlank(question.question)) { - body.addRow(rowIndex++, new Label(question.question), comboBox); + if (StringUtils.isNotBlank(question.question.get())) { + body.addRow(rowIndex++, new Label(question.question.get()), comboBox); } else { GridPane.setColumnSpan(comboBox, 2); body.addRow(rowIndex++, comboBox); @@ -99,7 +101,7 @@ public class PromptDialogPane extends DialogPane { } else if (question instanceof Builder.HintQuestion) { HintPane pane = new HintPane(); GridPane.setColumnSpan(pane, 2); - pane.setText(question.question); + pane.textProperty().bind(question.question); body.addRow(rowIndex++, pane); } } @@ -142,16 +144,28 @@ public class PromptDialogPane extends DialogPane { } public static class Question { - public final String question; + public final StringProperty question = new SimpleStringProperty(); protected T value; public Question(String question) { - this.question = question; + this.question.set(question); } public T getValue() { return value; } + + public String getQuestion() { + return question.get(); + } + + public StringProperty questionProperty() { + return question; + } + + public void setQuestion(String question) { + this.question.set(question); + } } public static class HintQuestion extends Question { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/CreateMultiplayerRoomDialog.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/CreateMultiplayerRoomDialog.java index cf38a2c33..b0feff0d7 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/CreateMultiplayerRoomDialog.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/CreateMultiplayerRoomDialog.java @@ -17,6 +17,9 @@ */ package org.jackhuang.hmcl.ui.multiplayer; +import com.jfoenix.controls.JFXCheckBox; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.control.Label; import javafx.scene.control.ProgressIndicator; import javafx.scene.layout.ColumnConstraints; @@ -35,12 +38,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAware { - private final FutureCallback callback; + private final FutureCallback callback; private final LocalServerDetector lanServerDetectorThread; + private final BooleanProperty allowAllJoinRequests = new SimpleBooleanProperty(true); private LocalServerDetector.PingResponse server; - CreateMultiplayerRoomDialog(FutureCallback callback) { + CreateMultiplayerRoomDialog(FutureCallback callback) { this.callback = callback; setTitle(i18n("multiplayer.session.create")); @@ -70,6 +74,11 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa portLabel.setText(i18n("multiplayer.nat.testing")); body.addRow(2, new Label(i18n("multiplayer.session.create.port")), portLabel); + JFXCheckBox allowAllJoinRequestsCheckBox = new JFXCheckBox(i18n("multiplayer.session.create.join.allow")); + allowAllJoinRequestsCheckBox.selectedProperty().bindBidirectional(allowAllJoinRequests); + GridPane.setColumnSpan(allowAllJoinRequestsCheckBox, 2); + body.addRow(3, allowAllJoinRequestsCheckBox); + setValid(false); JFXHyperlink noinLink = new JFXHyperlink(); @@ -102,7 +111,10 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa protected void onAccept() { setLoading(); - callback.call(Objects.requireNonNull(server), () -> { + callback.call(new CreationRequest( + Objects.requireNonNull(server), + allowAllJoinRequests.get() + ), () -> { runInFX(this::onSuccess); }, msg -> { runInFX(() -> onFailure(msg)); @@ -121,4 +133,21 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa lanServerDetectorThread.interrupt(); } + public static class CreationRequest { + private final LocalServerDetector.PingResponse server; + private final boolean allowAllJoinRequests; + + public CreationRequest(LocalServerDetector.PingResponse server, boolean allowAllJoinRequests) { + this.server = server; + this.allowAllJoinRequests = allowAllJoinRequests; + } + + public LocalServerDetector.PingResponse getServer() { + return server; + } + + public boolean isAllowAllJoinRequests() { + return allowAllJoinRequests; + } + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java index ec4028fa6..b6b540998 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java @@ -85,7 +85,7 @@ public final class MultiplayerManager { return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH); } - public static CompletableFuture joinSession(String token, String version, String sessionName, String peer, Mode mode, int remotePort, int localPort) throws IncompatibleCatoVersionException { + public static CompletableFuture joinSession(String token, String version, String sessionName, String peer, Mode mode, int remotePort, int localPort, JoinSessionHandler handler) throws IncompatibleCatoVersionException { if (!CATO_VERSION.equals(version)) { throw new IncompatibleCatoVersionException(version, CATO_VERSION); } @@ -133,6 +133,16 @@ public final class MultiplayerManager { MultiplayerClient client = new MultiplayerClient(session.getId(), localPort); session.addRelatedThread(client); session.setClient(client); + + if (handler != null) { + handler.onWaitingForJoinResponse(); + } + + TimerTask task = Lang.setTimeout(() -> { + future.completeExceptionally(new JoinRequestTimeoutException()); + session.stop(); + }, 30 * 1000); + client.onConnected().register(connectedEvent -> { try { int port = findAvailablePort(); @@ -148,10 +158,12 @@ public final class MultiplayerManager { future.completeExceptionally(e); session.stop(); } + task.cancel(); }); client.onKicked().register(kickedEvent -> { future.completeExceptionally(new CancellationException()); session.stop(); + task.cancel(); }); client.start(); }); @@ -160,7 +172,7 @@ public final class MultiplayerManager { }); } - public static CatoSession createSession(String token, String sessionName, int gamePort) throws IOException { + public static CatoSession createSession(String token, String sessionName, int gamePort, boolean allowAllJoinRequests) throws IOException { Path exe = getCatoExecutable(); if (!Files.isRegularFile(exe)) { throw new IllegalStateException("Cato file not found"); @@ -170,7 +182,7 @@ public final class MultiplayerManager { throw new CatoAlreadyStartedException(); } - MultiplayerServer server = new MultiplayerServer(gamePort); + MultiplayerServer server = new MultiplayerServer(gamePort, allowAllJoinRequests); server.startServer(); String[] commands = new String[]{exe.toString(), @@ -419,6 +431,10 @@ public final class MultiplayerManager { } } + public interface JoinSessionHandler { + void onWaitingForJoinResponse(); + } + public static class IncompatibleCatoVersionException extends Exception { private final String expected; private final String actual; @@ -448,4 +464,7 @@ public final class MultiplayerManager { public static class CatoAlreadyStartedException extends RuntimeException { } + + public static class JoinRequestTimeoutException extends RuntimeException { + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java index 7dfac8b86..e69f720ce 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java @@ -210,9 +210,9 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP } Controllers.dialog(new CreateMultiplayerRoomDialog((result, resolve, reject) -> { - int gamePort = result.getAd(); + int gamePort = result.getServer().getAd(); try { - MultiplayerManager.CatoSession session = MultiplayerManager.createSession(globalConfig().getMultiplayerToken(), result.getMotd(), gamePort); + MultiplayerManager.CatoSession session = MultiplayerManager.createSession(globalConfig().getMultiplayerToken(), result.getServer().getMotd(), gamePort, result.isAllowAllJoinRequests()); session.getServer().setOnClientAdding((client, resolveClient, rejectClient) -> { runInFX(() -> { Controllers.confirm(i18n("multiplayer.session.create.join.prompt", client.getUsername()), i18n("multiplayer.session.create.join"), MessageDialogPane.MessageType.INFO, @@ -251,6 +251,8 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP } Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.join"), (result, resolve, reject) -> { + PromptDialogPane.Builder.HintQuestion hintQuestion = (PromptDialogPane.Builder.HintQuestion) result.get(0); + String invitationCode = ((PromptDialogPane.Builder.StringQuestion) result.get(1)).getValue(); MultiplayerManager.Invitation invitation; try { @@ -279,7 +281,12 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP ? MultiplayerManager.Mode.RELAY : MultiplayerManager.Mode.P2P, invitation.getChannelPort(), - localPort) + localPort, new MultiplayerManager.JoinSessionHandler() { + @Override + public void onWaitingForJoinResponse() { + hintQuestion.setQuestion(i18n("multiplayer.session.join.wait")); + } + }) .thenAcceptAsync(session -> { initCatoSession(session); @@ -314,6 +321,10 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP LOG.info("Cato already started"); reject.accept(i18n("multiplayer.session.error.already_started")); return null; + } else if (throwable instanceof MultiplayerManager.JoinRequestTimeoutException) { + LOG.info("Cato already started"); + reject.accept(i18n("multiplayer.session.join.wait_timeout")); + return null; } else { LOG.log(Level.WARNING, "Failed to join sessoin"); reject.accept(i18n("multiplayer.session.join.error")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerServer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerServer.java index e146a43e7..98ac37805 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerServer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerServer.java @@ -37,6 +37,7 @@ import static org.jackhuang.hmcl.util.Logging.LOG; public class MultiplayerServer extends Thread { private ServerSocket socket; private final int gamePort; + private final boolean allowAllJoinRequests; private FutureCallback onClientAdding; private final EventManager onClientAdded = new EventManager<>(); @@ -46,8 +47,9 @@ public class MultiplayerServer extends Thread { private final Map clients = new ConcurrentHashMap<>(); private final Map nameClientMap = new ConcurrentHashMap<>(); - public MultiplayerServer(int gamePort) { + public MultiplayerServer(int gamePort, boolean allowAllJoinRequests) { this.gamePort = gamePort; + this.allowAllJoinRequests = allowAllJoinRequests; setName("MultiplayerServer"); setDaemon(true); @@ -146,7 +148,7 @@ public class MultiplayerServer extends Thread { nameClientMap.put(clientName, endpoint); onClientAdded.fireEvent(catoClient); - if (onClientAdding != null) { + if (onClientAdding != null && !allowAllJoinRequests) { onClientAdding.call(catoClient, () -> { try { endpoint.write(new JoinResponse(gamePort)); @@ -168,6 +170,9 @@ public class MultiplayerServer extends Thread { LOG.log(Level.WARNING, "Failed to send kick response.", e); } }); + } else { + // Allow all join requests. + endpoint.write(new JoinResponse(gamePort)); } } else if (request instanceof KeepAliveRequest) { endpoint.write(new KeepAliveResponse(System.currentTimeMillis())); diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index a3e0b4515..6f361b80a 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -652,6 +652,7 @@ multiplayer.session.create=Create Session multiplayer.session.create.error=Failed to create multiplayer session. multiplayer.session.create.hint=Before creating multiplayer session, you must click "Open LAN Server" in running game, and type the port displayed in game in the blank below. multiplayer.session.create.join=Connection request +multiplayer.session.create.join.allow=Accepts all join requests (When disabled, you need to accept join requests manually, so you can prevent from attacks.) multiplayer.session.create.join.prompt=Player %s wants to join the multiplayer session. Accept? multiplayer.session.create.members=Members multiplayer.session.create.members.kick=Kick @@ -675,6 +676,8 @@ multiplayer.session.join.kicked=You have been kicked by the session holder. You multiplayer.session.join.lost_connection=Lost connection with the multiplayer session. Maybe the session is destroyed by the creator, or you cannot establish connection with this session. multiplayer.session.join.port.error=Cannot find available local network port for listening. Please ensure that HMCL has the permission to listen on a port. multiplayer.session.join.rejected=Your connection is rejected by the session holder. +multiplayer.session.join.wait=Waiting for acceptation. +multiplayer.session.join.wait_timeout=The session holder does not accept your join request in time. multiplayer.session.members=Room Members multiplayer.session.quit=Quit Room multiplayer.session.quit.warning=After quiting room, you will lost the connection with the server. Continue? diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index e08b5e1d0..fa86a9e98 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -652,6 +652,7 @@ multiplayer.session.create=創建房間 multiplayer.session.create.error=創建聯機房間失敗。 multiplayer.session.create.hint=創建聯機房間前,你需要先在正在運行的遊戲內的遊戲菜單中選擇 對區域網路開放 選項,然後在下方的輸入框中輸入遊戲內提示的埠號(通常是 5 位的數字) multiplayer.session.create.join=連接申請 +multiplayer.session.create.join.allow=自動接受所有連接申請(不啟用此選項時,你需要手動同意申請,以避免不相關人士誤連你的伺服器) multiplayer.session.create.join.prompt=玩家 %s 申請加入多人聯機房間,是否接受? multiplayer.session.create.members=成員 multiplayer.session.create.members.kick=踢出房間 @@ -674,6 +675,8 @@ multiplayer.session.join.kicked=你已經被房主踢出房間,你將與房間 multiplayer.session.join.lost_connection=你已與房間失去連接。這可能意味著房主已經解散房間,或者你無法連接至房間。 multiplayer.session.join.port.error=無法找到可用的本地網路埠,請確保 HMCL 擁有綁定本地埠的權限。 multiplayer.session.join.rejected=你被房主拒絕連接。 +multiplayer.session.join.wait=等待對方同意加入申請。 +multiplayer.session.join.wait_timeout=對方未能即時同意你的加入申請 multiplayer.session.members=房間成員 multiplayer.session.quit=退出房間 multiplayer.session.quit.warning=退出房間後,你將會與伺服器斷開連接,是否繼續? diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index d02299168..8163a6ead 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -652,6 +652,7 @@ multiplayer.session.create=创建房间 multiplayer.session.create.error=创建联机房间失败。 multiplayer.session.create.hint=创建联机房间前,你需要先在正在运行的游戏内的游戏菜单中选择 对局域网开放 选项,然后在下方的输入框中确认游戏内提示的端口号(通常是 5 位的数字) multiplayer.session.create.join=连接申请 +multiplayer.session.create.join.allow=自动接受所有连接申请(不启用此选项时,你需要手动同意申请,以避免不相关人士误连你的服务器) multiplayer.session.create.join.prompt=玩家 %s 申请加入多人联机房间,是否接受? multiplayer.session.create.members=成员 multiplayer.session.create.members.kick=踢出房间 @@ -674,6 +675,8 @@ multiplayer.session.join.kicked=你已经被房主踢出房间,你将与房间 multiplayer.session.join.lost_connection=你已与房间失去连接。这可能意味着房主已经解散房间,或者你无法连接至房间。 multiplayer.session.join.port.error=无法找到可用的本地网络端口,请确保 HMCL 拥有绑定本地端口的权限。 multiplayer.session.join.rejected=你被房主拒绝连接。 +multiplayer.session.join.wait=等待对方同意加入申请。 +multiplayer.session.join.wait_timeout=对方未能即时同意你的加入申请 multiplayer.session.members=房间成员 multiplayer.session.quit=退出房间 multiplayer.session.quit.warning=退出房间后,你将会与服务器断开连接,是否继续? diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java index 570b49f2f..c898acf2c 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Lang.java @@ -357,6 +357,22 @@ public final class Lang { return () -> iterator; } + private static Timer timer; + + public static synchronized TimerTask setTimeout(Runnable runnable, long delayMs) { + if (timer == null) { + timer = new Timer(); + } + TimerTask task = new TimerTask() { + @Override + public void run() { + runnable.run(); + } + }; + timer.schedule(task, delayMs); + return task; + } + /** * This is a useful function to prevent exceptions being eaten when using CompletableFuture. * You can write: