diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerChannel.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerChannel.java index ee7a0d940..b8fd29d88 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerChannel.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerChannel.java @@ -29,6 +29,7 @@ public final class MultiplayerChannel { @JsonType( property = "type", subtypes = { + @JsonSubtype(clazz = HandshakeRequest.class, name = "handshake"), @JsonSubtype(clazz = JoinRequest.class, name = "join"), @JsonSubtype(clazz = KeepAliveRequest.class, name = "keepalive") } @@ -36,6 +37,9 @@ public final class MultiplayerChannel { public static class Request { } + public static class HandshakeRequest extends Request { + } + public static class JoinRequest extends Request { private final String clientVersion; private final String username; @@ -69,6 +73,7 @@ public final class MultiplayerChannel { @JsonType( property = "type", subtypes = { + @JsonSubtype(clazz = HandshakeResponse.class, name = "handshake"), @JsonSubtype(clazz = JoinResponse.class, name = "join"), @JsonSubtype(clazz = KeepAliveResponse.class, name = "keepalive"), @JsonSubtype(clazz = KickResponse.class, name = "kick") @@ -78,6 +83,9 @@ public final class MultiplayerChannel { } + public static class HandshakeResponse extends Response { + } + public static class JoinResponse extends Response { private final int port; diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClient.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClient.java index e8377c0f1..3757e43a3 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClient.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClient.java @@ -20,12 +20,14 @@ package org.jackhuang.hmcl.ui.multiplayer; import com.google.gson.JsonParseException; import org.jackhuang.hmcl.event.Event; import org.jackhuang.hmcl.event.EventManager; +import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.gson.JsonUtils; import java.io.*; import java.net.ConnectException; import java.net.InetAddress; import java.net.Socket; +import java.util.TimerTask; import java.util.logging.Level; import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.*; @@ -36,10 +38,12 @@ public class MultiplayerClient extends Thread { private final int port; private int gamePort; + private boolean connected = false; private final EventManager onConnected = new EventManager<>(); private final EventManager onDisconnected = new EventManager<>(); private final EventManager onKicked = new EventManager<>(); + private final EventManager onHandshake = new EventManager<>(); public MultiplayerClient(String id, int port) { this.id = id; @@ -66,7 +70,11 @@ public class MultiplayerClient extends Thread { } public EventManager onKicked() { - return onDisconnected; + return onKicked; + } + + public EventManager onHandshake() { + return onHandshake; } @Override @@ -80,15 +88,25 @@ public class MultiplayerClient extends Thread { MultiplayerServer.Endpoint endpoint = new MultiplayerServer.Endpoint(socket, writer); LOG.info("Connected to 127.0.0.1:" + port); - writer.write(JsonUtils.UGLY_GSON.toJson(new JoinRequest(MultiplayerManager.CATO_VERSION, id))); - writer.newLine(); - writer.flush(); + endpoint.write(new HandshakeRequest()); + endpoint.write(new JoinRequest(MultiplayerManager.CATO_VERSION, id)); LOG.fine("Sent join request with id=" + id); keepAliveThread = new KeepAliveThread(endpoint); keepAliveThread.start(); + TimerTask task = Lang.setTimeout(() -> { + // If after 15 seconds, we didn't receive the HandshakeResponse, + // We fail to establish the connection with server. + + try { + socket.close(); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to close socket", e); + } + }, 15 * 1000); + String line; while ((line = reader.readLine()) != null) { if (isInterrupted()) { @@ -102,14 +120,21 @@ public class MultiplayerClient extends Thread { if (response instanceof JoinResponse) { JoinResponse joinResponse = JsonUtils.fromNonNullJson(line, JoinResponse.class); setGamePort(joinResponse.getPort()); + + connected = true; + onConnected.fireEvent(new ConnectedEvent(this, joinResponse.getPort())); LOG.fine("Received join response with port " + joinResponse.getPort()); } else if (response instanceof KickResponse) { - onKicked.fireEvent(new Event(this)); - LOG.fine("Kicked by the server"); + onKicked.fireEvent(new Event(this)); + return; } else if (response instanceof KeepAliveResponse) { + } else if (response instanceof HandshakeResponse) { + LOG.fine("Established connection with server"); + onHandshake.fireEvent(new Event(this)); + task.cancel(); } else { LOG.log(Level.WARNING, "Unrecognized packet from server:" + line); } @@ -135,6 +160,10 @@ public class MultiplayerClient extends Thread { onDisconnected.fireEvent(new Event(this)); } + public boolean isConnected() { + return connected; + } + private static class KeepAliveThread extends Thread { private final MultiplayerServer.Endpoint endpoint; 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 aa90b0f3a..f2e578683 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 @@ -131,10 +131,6 @@ public final class MultiplayerManager { session.addRelatedThread(client); session.setClient(client); - if (handler != null) { - handler.onWaitingForJoinResponse(); - } - TimerTask task = Lang.setTimeout(() -> { future.completeExceptionally(new JoinRequestTimeoutException()); session.stop(); @@ -162,6 +158,17 @@ public final class MultiplayerManager { session.stop(); task.cancel(); }); + client.onDisconnected().register(disconnectedEvent -> { + if (!client.isConnected()) { + // We fail to establish connection with server + future.completeExceptionally(new ConnectionErrorException()); + } + }); + client.onHandshake().register(handshakeEvent -> { + if (handler != null) { + handler.onWaitingForJoinResponse(); + } + }); client.start(); }); @@ -464,4 +471,7 @@ public final class MultiplayerManager { public static class JoinRequestTimeoutException extends RuntimeException { } + + public static class ConnectionErrorException 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 7b9862f73..855f72be4 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 @@ -132,6 +132,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP FastDiscoveryTest tester = new FastDiscoveryTest(null, 0, "stun.qq.com", 3478); return tester.test(); }).whenComplete(Schedulers.javafx(), (info, exception) -> { + LOG.log(Level.INFO, "Nat test result " + MultiplayerPageSkin.getNATType(info), exception); if (exception == null) { natState.set(info); } else { @@ -292,7 +293,9 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP localPort, new MultiplayerManager.JoinSessionHandler() { @Override public void onWaitingForJoinResponse() { - hintQuestion.setQuestion(i18n("multiplayer.session.join.wait")); + runInFX(() -> { + hintQuestion.setQuestion(i18n("multiplayer.session.join.wait")); + }); } }) .thenAcceptAsync(session -> { @@ -338,6 +341,10 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP LOG.info("Cato already started"); reject.accept(i18n("multiplayer.session.join.wait_timeout")); return null; + } else if (resolved instanceof MultiplayerManager.ConnectionErrorException) { + LOG.info("Failed to establish connection with server"); + reject.accept(i18n("multiplayer.session.join.error.connection")); + return null; } else { LOG.log(Level.WARNING, "Failed to join session", resolved); reject.accept(i18n("multiplayer.session.join.error")); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java index ad5821ce1..46e5b8333 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java @@ -295,7 +295,7 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated } } - private static String getNATType(DiscoveryInfo info) { + public static String getNATType(DiscoveryInfo info) { if (info == null) { return i18n("multiplayer.nat.testing"); } else if (info.isBlockedUDP()) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 24b0ca35d..6d0dc446c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -669,6 +669,7 @@ multiplayer.session.expired=Multiplayer session has expired. You should re-creat multiplayer.session.hint=You must click "Open LAN Server" in game in order to enable multiplayer functionality. multiplayer.session.join=Join Session multiplayer.session.join.error=Failed to join multiplayer session +multiplayer.session.join.error.connection=Failed to join multiplayer session. Cannot establish connection. multiplayer.session.join.hint=You must obtain the invitation code from the gamer who has already created a multiplayer session. multiplayer.session.join.invitation_code=Invitation code multiplayer.session.join.invitation_code.error=Incorrect invitation code. Please obtain invitation code from the player who creates the multiplayer session. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 3896a0fce..2d2589d9c 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -668,6 +668,7 @@ multiplayer.session.error.file_not_found=找不到 cato。請檢查防毒軟體 multiplayer.session.expired=聯機會話連續使用時間超過了 3 小時,你需要重新創建/加入房間以繼續聯機。 multiplayer.session.join=加入房間 multiplayer.session.join.error=加入房間失敗 +multiplayer.session.join.error.connection=加入房間失敗。無法與對方建立連接。如果你或對方的網路類型是差(對稱型),可能無法使用聯機功能。 multiplayer.session.join.hint=你需要向已經創建好房間的玩家索要邀請碼以便加入多人聯機房間 multiplayer.session.join.invitation_code=邀請碼 multiplayer.session.join.invitation_code.error=邀請碼不正確,請向開服玩家獲取邀請碼 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 ebfd0438f..4c0cd8212 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -669,6 +669,7 @@ multiplayer.session.error.file_not_found=找不到 cato。请检查杀毒软件 multiplayer.session.expired=联机会话连续使用时间超过了 3 小时,你需要重新创建/加入房间以继续联机。 multiplayer.session.join=加入房间 multiplayer.session.join.error=加入房间失败。如果你或对方的网络类型是差(对称型),可能无法使用联机功能。 +multiplayer.session.join.error.connection=加入房间失败。无法与对方建立连接。如果你或对方的网络类型是差(对称型),可能无法使用联机功能。 multiplayer.session.join.hint=你需要向已经创建好房间的玩家索要邀请码以便加入多人联机房间 multiplayer.session.join.invitation_code=邀请码 multiplayer.session.join.invitation_code.error=邀请码不正确,请向开服玩家获取邀请码