diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java index 56288bf0d..23beaad00 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/GlobalConfig.java @@ -21,10 +21,7 @@ import com.google.gson.*; import com.google.gson.annotations.JsonAdapter; import javafx.beans.InvalidationListener; import javafx.beans.Observable; -import javafx.beans.property.IntegerProperty; -import javafx.beans.property.SimpleIntegerProperty; -import javafx.beans.property.SimpleStringProperty; -import javafx.beans.property.StringProperty; +import javafx.beans.property.*; import javafx.collections.ObservableList; import javafx.collections.ObservableMap; import javafx.collections.ObservableSet; @@ -73,6 +70,8 @@ public class GlobalConfig implements Cloneable, Observable { private StringProperty multiplayerToken = new SimpleStringProperty(); + private BooleanProperty multiplayerRelay = new SimpleBooleanProperty(); + private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0); private final Map unknownFields = new HashMap<>(); @@ -114,6 +113,18 @@ public class GlobalConfig implements Cloneable, Observable { this.agreementVersion.set(agreementVersion); } + public boolean isMultiplayerRelay() { + return multiplayerRelay.get(); + } + + public BooleanProperty multiplayerRelayProperty() { + return multiplayerRelay; + } + + public void setMultiplayerRelay(boolean multiplayerRelay) { + this.multiplayerRelay.set(multiplayerRelay); + } + public int getMultiplayerAgreementVersion() { return multiplayerAgreementVersion.get(); } @@ -154,6 +165,7 @@ public class GlobalConfig implements Cloneable, Observable { JsonObject jsonObject = new JsonObject(); jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion())); jsonObject.add("multiplayerToken", context.serialize(src.getMultiplayerToken())); + jsonObject.add("multiplayerRelay", context.serialize(src.isMultiplayerRelay())); jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion())); for (Map.Entry entry : src.unknownFields.entrySet()) { jsonObject.add(entry.getKey(), context.serialize(entry.getValue())); @@ -171,6 +183,7 @@ public class GlobalConfig implements Cloneable, Observable { GlobalConfig config = new GlobalConfig(); config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0)); config.setMultiplayerToken(Optional.ofNullable(obj.get("multiplayerToken")).map(JsonElement::getAsString).orElse(null)); + config.setMultiplayerRelay(Optional.ofNullable(obj.get("multiplayerRelay")).map(JsonElement::getAsBoolean).orElse(false)); config.setMultiplayerAgreementVersion(Optional.ofNullable(obj.get("multiplayerAgreementVersion")).map(JsonElement::getAsInt).orElse(0)); for (Map.Entry entry : obj.entrySet()) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java index b13c137fd..0e90c857a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/HMCLAccounts.java @@ -33,10 +33,13 @@ import java.util.UUID; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Pair.pair; -public class HMCLAccounts { +public final class HMCLAccounts { private static final ObjectProperty account = new SimpleObjectProperty<>(); + private HMCLAccounts() { + } + public static HMCLAccount getAccount() { return account.get(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/OptionToggleButton.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/OptionToggleButton.java index 188abfacc..741a72a12 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/OptionToggleButton.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/construct/OptionToggleButton.java @@ -27,10 +27,13 @@ import javafx.geometry.Pos; import javafx.scene.control.Label; import javafx.scene.layout.BorderPane; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import org.jackhuang.hmcl.ui.FXUtils; +import org.jackhuang.hmcl.util.StringUtils; public class OptionToggleButton extends StackPane { private final StringProperty title = new SimpleStringProperty(); + private final StringProperty subtitle = new SimpleStringProperty(); private final BooleanProperty selected = new SimpleBooleanProperty(); public OptionToggleButton() { @@ -41,11 +44,15 @@ public class OptionToggleButton extends StackPane { RipplerContainer container = new RipplerContainer(pane); getChildren().setAll(container); - Label label = new Label(); - label.setMouseTransparent(true); - label.textProperty().bind(title); - pane.setLeft(label); - BorderPane.setAlignment(label, Pos.CENTER_LEFT); + VBox left = new VBox(); + Label titleLabel = new Label(); + titleLabel.setMouseTransparent(true); + titleLabel.textProperty().bind(title); + Label subtitleLabel = new Label(); + subtitleLabel.setMouseTransparent(true); + subtitleLabel.textProperty().bind(subtitle); + pane.setLeft(left); + BorderPane.setAlignment(left, Pos.CENTER_LEFT); JFXToggleButton toggleButton = new JFXToggleButton(); pane.setRight(toggleButton); @@ -56,6 +63,14 @@ public class OptionToggleButton extends StackPane { container.setOnMouseClicked(e -> { toggleButton.setSelected(!toggleButton.isSelected()); }); + + FXUtils.onChangeAndOperate(subtitleProperty(), subtitle -> { + if (StringUtils.isNotBlank(subtitle)) { + left.getChildren().setAll(titleLabel, subtitleLabel); + } else { + left.getChildren().setAll(titleLabel); + } + }); } public String getTitle() { @@ -70,6 +85,18 @@ public class OptionToggleButton extends StackPane { this.title.set(title); } + public String getSubtitle() { + return subtitle.get(); + } + + public StringProperty subtitleProperty() { + return subtitle; + } + + public void setSubtitle(String subtitle) { + this.subtitle.set(subtitle); + } + public boolean isSelected() { return selected.get(); } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java index 706c742d9..be5fbfc5b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/FeedbackPage.java @@ -183,7 +183,7 @@ public class FeedbackPage extends VBox implements PageAware { } HttpRequest req = HttpRequest.GET(NetworkUtils.withQuery("https://hmcl.huangyuhui.net/api/feedback", query)); if (account != null) { - req.authorization("Bearer", HMCLAccounts.getAccount().getIdToken()) + req.authorization("Bearer", HMCLAccounts.getAccount().getIdToken()) .header("Authorization-Provider", HMCLAccounts.getAccount().getProvider()); } return req.>getJson(new TypeToken>(){}.getType()); diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java index bfed711d4..33619b809 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/main/RootPage.java @@ -53,7 +53,6 @@ import java.util.Date; import java.util.List; import java.util.stream.Collectors; -import static org.jackhuang.hmcl.ui.FXUtils.newImage; import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap; import static org.jackhuang.hmcl.util.i18n.I18n.i18n; 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 6b81dcabf..e8377c0f1 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 @@ -73,19 +73,22 @@ public class MultiplayerClient extends Thread { public void run() { LOG.info("Connecting to 127.0.0.1:" + port); for (int i = 0; i < 5; i++) { + KeepAliveThread keepAliveThread = null; try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) { + MultiplayerServer.Endpoint endpoint = new MultiplayerServer.Endpoint(socket, writer); LOG.info("Connected to 127.0.0.1:" + port); - socket.setKeepAlive(true); - writer.write(JsonUtils.UGLY_GSON.toJson(new JoinRequest(MultiplayerManager.CATO_VERSION, id))); writer.newLine(); writer.flush(); LOG.fine("Sent join request with id=" + id); + keepAliveThread = new KeepAliveThread(endpoint); + keepAliveThread.start(); + String line; while ((line = reader.readLine()) != null) { if (isInterrupted()) { @@ -106,6 +109,7 @@ public class MultiplayerClient extends Thread { onKicked.fireEvent(new Event(this)); LOG.fine("Kicked by the server"); + } else if (response instanceof KeepAliveResponse) { } else { LOG.log(Level.WARNING, "Unrecognized packet from server:" + line); } @@ -121,12 +125,42 @@ public class MultiplayerClient extends Thread { continue; } catch (IOException | JsonParseException e) { e.printStackTrace(); + } finally { + if (keepAliveThread != null) { + keepAliveThread.interrupt(); + } } } LOG.info("Lost connection to 127.0.0.1:" + port); onDisconnected.fireEvent(new Event(this)); } + private static class KeepAliveThread extends Thread { + + private final MultiplayerServer.Endpoint endpoint; + + public KeepAliveThread(MultiplayerServer.Endpoint endpoint) { + this.endpoint = endpoint; + } + + @Override + public void run() { + while (!isInterrupted()) { + try { + endpoint.write(new KeepAliveRequest(System.currentTimeMillis())); + } catch (IOException e) { + LOG.log(Level.WARNING, "Failed to send keep alive packet", e); + break; + } + try { + Thread.sleep(1500); + } catch (InterruptedException e) { + break; + } + } + } + } + public static class ConnectedEvent extends Event { private final int port; 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 917c3688d..c6cd62a36 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 @@ -44,10 +44,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; -import java.util.Arrays; -import java.util.Base64; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.logging.Level; @@ -67,7 +64,6 @@ public final class MultiplayerManager { private static final String REMOTE_ADDRESS = "127.0.0.1"; private static final String LOCAL_ADDRESS = "0.0.0.0"; - private static final String MODE = "p2p"; private MultiplayerManager() { } @@ -89,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, int remotePort, int localPort) throws IncompatibleCatoVersionException { + public static CompletableFuture joinSession(String token, String version, String sessionName, String peer, Mode mode, int remotePort, int localPort) throws IncompatibleCatoVersionException { if (!CATO_VERSION.equals(version)) { throw new IncompatibleCatoVersionException(version, CATO_VERSION); } @@ -100,12 +96,16 @@ public final class MultiplayerManager { } return CompletableFuture.completedFuture(null).thenComposeAsync(unused -> { + if (!isPortAvailable(3478)) { + throw new CatoAlreadyStartedException(); + } + String[] commands = new String[]{exe.toString(), "--token", StringUtils.isBlank(token) ? "new" : token, "--id", peer, "--local", String.format("%s:%d", LOCAL_ADDRESS, localPort), "--remote", String.format("%s:%d", REMOTE_ADDRESS, remotePort), - "--mode", MODE}; + "--mode", mode.getName()}; Process process; try { process = new ProcessBuilder() @@ -137,18 +137,21 @@ public final class MultiplayerManager { client.onConnected().register(connectedEvent -> { try { int port = findAvailablePort(); - String command = String.format("net add %s %s:%d %s:%d %s\n", peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort(), MODE); + String command = String.format("net add %s %s:%d %s:%d %s\n", peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort(), mode.getName()); LOG.info("Invoking cato: " + command); + client.setGamePort(port); writer.write(command); writer.newLine(); writer.flush(); future.complete(session); } catch (IOException e) { future.completeExceptionally(e); + session.stop(); } }); client.onKicked().register(kickedEvent -> { future.completeExceptionally(new CancellationException()); + session.stop(); }); client.start(); }); @@ -163,13 +166,16 @@ public final class MultiplayerManager { throw new IllegalStateException("Cato file not found"); } + if (!isPortAvailable(3478)) { + throw new CatoAlreadyStartedException(); + } + MultiplayerServer server = new MultiplayerServer(gamePort); server.startServer(); String[] commands = new String[]{exe.toString(), "--token", StringUtils.isBlank(token) ? "new" : token, - "--allows", String.format("%s:%d/%s:%d", REMOTE_ADDRESS, server.getPort(), REMOTE_ADDRESS, gamePort), - "--mode", MODE}; + "--allows", String.format("%s:%d/%s:%d", REMOTE_ADDRESS, server.getPort(), REMOTE_ADDRESS, gamePort)}; Process process = new ProcessBuilder() .command(commands) .start(); @@ -191,6 +197,14 @@ public final class MultiplayerManager { } } + public static boolean isPortAvailable(int port) { + try (ServerSocket socket = new ServerSocket(port)) { + return true; + } catch (IOException e) { + return false; + } + } + public static String getCatoPath() { switch (OperatingSystem.CURRENT_OS) { case WINDOWS: @@ -420,4 +434,16 @@ public final class MultiplayerManager { return actual; } } + + public enum Mode { + P2P, + RELAY; + + String getName() { + return name().toLowerCase(Locale.ROOT); + } + } + + public static class CatoAlreadyStartedException 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 2f0f3c7ff..90a3ca90c 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 @@ -227,6 +227,9 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware }); }); initCatoSession(session); + } catch (MultiplayerManager.CatoAlreadyStartedException e) { + LOG.log(Level.WARNING, "Cato already started", e); + reject.accept(i18n("multiplayer.session.error.already_started")); } catch (Exception e) { LOG.log(Level.WARNING, "Failed to create session", e); reject.accept(i18n("multiplayer.session.create.error")); @@ -264,7 +267,16 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware } try { - MultiplayerManager.joinSession(globalConfig().getMultiplayerToken(), invitation.getVersion(), invitation.getSessionName(), invitation.getId(), invitation.getChannelPort(), localPort) + MultiplayerManager.joinSession( + globalConfig().getMultiplayerToken(), + invitation.getVersion(), + invitation.getSessionName(), + invitation.getId(), + globalConfig().isMultiplayerRelay() && StringUtils.isNotBlank(globalConfig().getMultiplayerToken()) + ? MultiplayerManager.Mode.RELAY + : MultiplayerManager.Mode.P2P, + invitation.getChannelPort(), + localPort) .thenAcceptAsync(session -> { initCatoSession(session); @@ -295,9 +307,13 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware LOG.info("Connection rejected by the server"); reject.accept(i18n("multiplayer.session.join.rejected")); return null; + } else if (throwable instanceof MultiplayerManager.CatoAlreadyStartedException) { + LOG.info("Cato already started"); + reject.accept(i18n("multiplayer.session.error.already_started")); + return null; } else { LOG.log(Level.WARNING, "Failed to join sessoin"); - reject.accept(i18n("multiplayer.session.error")); + reject.accept(i18n("multiplayer.session.join.error")); } return null; }); 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 1c59ed8c9..7c5625a16 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 @@ -265,6 +265,12 @@ public class MultiplayerPageSkin extends SkinBase { gridPane.addRow(0, new Label(i18n("multiplayer.session.create.token")), tokenField, applyLink); + OptionToggleButton relay = new OptionToggleButton(); + relay.disableProperty().bind(tokenField.textProperty().isEmpty()); + relay.selectedProperty().bindBidirectional(globalConfig().multiplayerRelayProperty()); + relay.setTitle(i18n("multiplayer.relay")); + relay.setSubtitle(i18n("multiplayer.relay.hint")); + HBox pane = new HBox(); pane.setAlignment(Pos.CENTER_LEFT); @@ -280,7 +286,7 @@ public class MultiplayerPageSkin extends SkinBase { placeholder, FXUtils.segmentToTextFlow(i18n("multiplayer.powered_by"), Controllers::onHyperlinkAction)); - thanksPane.getContent().addAll(gridPane, pane); + thanksPane.getContent().addAll(gridPane, relay, pane); } content.getChildren().setAll( 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 64016dd18..e146a43e7 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 @@ -18,6 +18,7 @@ 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.FutureCallback; import org.jackhuang.hmcl.util.Lang; @@ -40,9 +41,10 @@ public class MultiplayerServer extends Thread { private FutureCallback onClientAdding; private final EventManager onClientAdded = new EventManager<>(); private final EventManager onClientDisconnected = new EventManager<>(); + private final EventManager onKeepAlive = new EventManager<>(); - private final Map clients = new ConcurrentHashMap<>(); - private final Map nameClientMap = new ConcurrentHashMap<>(); + private final Map clients = new ConcurrentHashMap<>(); + private final Map nameClientMap = new ConcurrentHashMap<>(); public MultiplayerServer(int gamePort) { this.gamePort = gamePort; @@ -63,6 +65,10 @@ public class MultiplayerServer extends Thread { return onClientDisconnected; } + public EventManager onKeepAlive() { + return onKeepAlive; + } + public void startServer() throws IOException { startServer(0); } @@ -98,7 +104,7 @@ public class MultiplayerServer extends Thread { } public void kickPlayer(CatoClient player) { - Client client = nameClientMap.get(player.getUsername()); + Endpoint client = nameClientMap.get(player.getUsername()); if (client == null) return; try { @@ -119,8 +125,8 @@ public class MultiplayerServer extends Thread { BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) { clientSocket.setKeepAlive(true); - Client client = new Client(clientSocket, writer); - clients.put(address, client); + Endpoint endpoint = new Endpoint(clientSocket, writer); + clients.put(address, endpoint); String line; while ((line = reader.readLine()) != null) { @@ -137,13 +143,13 @@ public class MultiplayerServer extends Thread { clientName = joinRequest.getUsername(); CatoClient catoClient = new CatoClient(this, clientName); - nameClientMap.put(clientName, client); + nameClientMap.put(clientName, endpoint); onClientAdded.fireEvent(catoClient); if (onClientAdding != null) { onClientAdding.call(catoClient, () -> { try { - client.write(new JoinResponse(gamePort)); + endpoint.write(new JoinResponse(gamePort)); } catch (IOException e) { LOG.log(Level.WARNING, "Failed to send join response.", e); try { @@ -155,7 +161,7 @@ public class MultiplayerServer extends Thread { } }, msg -> { try { - client.write(new KickResponse(msg)); + endpoint.write(new KickResponse(msg)); LOG.info("Rejected join request from id=" + joinRequest.getUsername()); socket.close(); } catch (IOException e) { @@ -163,6 +169,10 @@ public class MultiplayerServer extends Thread { } }); } + } else if (request instanceof KeepAliveRequest) { + endpoint.write(new KeepAliveResponse(System.currentTimeMillis())); + + onKeepAlive.fireEvent(new Event(this)); } else { LOG.log(Level.WARNING, "Unrecognized packet from client " + targetSocket.getRemoteSocketAddress() + ":" + line); } @@ -181,11 +191,11 @@ public class MultiplayerServer extends Thread { } } - private static class Client { + public static class Endpoint { public final Socket socket; public final BufferedWriter writer; - public Client(Socket socket, BufferedWriter writer) { + public Endpoint(Socket socket, BufferedWriter writer) { this.socket = socket; this.writer = writer; } diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 250ea99cd..d0b1656c4 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -624,15 +624,17 @@ multiplayer.nat.type.symmetric_udp_firewall=Bad (Symmetric with UDP Firewall) multiplayer.nat.type.unknown=Unknown multiplayer.powered_by=Multiplayer service is provided by noin.cn. EULA multiplayer.report=Report +multiplayer.relay=Relay Mode +multiplayer.relay.hint=For users who has a static token, and whose tested NAT type is bad-symmetric multiplayer.session=Room multiplayer.session.name.format=%1$s's Room multiplayer.session.name.motd=HMCL Multiplayer Session - %s multiplayer.session.close=Close Room multiplayer.session.close.warning=After closing room, all players joined the room will lost connection. Continue? multiplayer.session.copy_room_code=Copy Invitation Code -multiplayer.session.create=Create Room -multiplayer.session.create.error=Failed to create multiplayer room. -multiplayer.session.create.hint=Before creating multiplayer room, you must click "Open LAN Server" in running game, and type the port displayed in game in the blank below. +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.prompt=Player %s wants to join the multiplayer session. Accept? multiplayer.session.create.members=Members @@ -642,14 +644,16 @@ multiplayer.session.create.name=Session Name multiplayer.session.create.port=Port multiplayer.session.create.port.error=Cannot detect game port, you must click "Open LAN Server" in game to enable multiplayer functionality. multiplayer.session.create.token=Token -multiplayer.session.create.token.apply=Apple for static token +multiplayer.session.create.token.apply=Apply for static token multiplayer.session.create.token.prompt=Default randomized. You can apply for your own token at noin.cn (Chinese website). +multiplayer.session.error.already_started=Cato service has already started locally. Please check whether there are other multiplayer services served or not. Or you can kill cato process in task manager. multiplayer.session.expired=Multiplayer session has expired. You should re-create or re-join a room to continue. multiplayer.session.hint=You must click "Open LAN Server" in game in order to enable multiplayer functionality. -multiplayer.session.join=Join Room -multiplayer.session.join.hint=You must obtain the invitation code from the gamer who has already created a multiplayer room. +multiplayer.session.join=Join Session +multiplayer.session.join.error=Failed to join multiplayer session +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 room. +multiplayer.session.join.invitation_code.error=Incorrect invitation code. Please obtain invitation code from the player who creates the multiplayer session. multiplayer.session.join.invitation_code.version=Versions of multiplayer functionalities are not the same among you. multiplayer.session.join.kicked=You have been kicked by the session holder. You will lost connection with the multiplayer session. 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. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 51d8121f1..e9a2b6274 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -624,6 +624,8 @@ multiplayer.nat.type.symmetric_udp_firewall=差(對稱型+防火牆) multiplayer.nat.type.unknown=未知 multiplayer.powered_by=多人聯機服務由 這裡 (noin.cn) 提供。用戶協議與免責聲明 multiplayer.report=違法違規檢舉 +multiplayer.relay=中繼模式 +multiplayer.relay.hint=NAT 網路類型為差-對稱型的用戶,若無法聯機,申請靜態 Token 後可以啟用本選項 multiplayer.session=房間 multiplayer.session.name.format=%1$s 的房間 multiplayer.session.name.motd=HMCL 多人聯機房間 - %s @@ -644,8 +646,10 @@ multiplayer.session.create.port.error=無法檢測遊戲埠號,你必須先啟 multiplayer.session.create.token=Token multiplayer.session.create.token.apply=申請靜態 Token multiplayer.session.create.token.prompt=預設為臨時 Token。你可以在 noin.cn 上申請靜態 Token 並填寫至此處 +multiplayer.session.error.already_started=本地已經開啟 cato 服務,請檢查是否有其他 HMCL 正在運行聯機服務。或者你可以在任務管理器裡殺掉 cato 進程以繼續。 multiplayer.session.expired=聯機會話連續使用時間超過了 3 小時,你需要重新創建/加入房間以繼續聯機。 multiplayer.session.join=加入房間 +multiplayer.session.join.error=加入房間失敗 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 c8c8c3bb0..3a1a8a96e 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -623,6 +623,8 @@ multiplayer.nat.type.symmetric=差(对称型) multiplayer.nat.type.symmetric_udp_firewall=差(对称型+防火墙) multiplayer.nat.type.unknown=未知 multiplayer.powered_by=多人联机服务由 这里 (noin.cn) 提供。用户协议与免责声明 +multiplayer.relay=中继模式 +multiplayer.relay.hint=NAT 网络类型为差-对称型的用户,若无法联机,申请静态 Token 后可以启用本选项 multiplayer.report=违法违规举报 multiplayer.session=房间 multiplayer.session.name.format=%1$s 的房间 @@ -644,8 +646,10 @@ multiplayer.session.create.port.error=无法检测游戏端口号,你必须先 multiplayer.session.create.token=Token multiplayer.session.create.token.apply=申请静态 Token multiplayer.session.create.token.prompt=默认为临时 Token。你可以在 noin.cn 上申请静态 Token 并填写至此处 +multiplayer.session.error.already_started=本地已经开启 cato 服务,请检查是否有其他 HMCL 正在运行联机服务。或者你可以在任务管理器里杀掉 cato 进程以继续。 multiplayer.session.expired=联机会话连续使用时间超过了 3 小时,你需要重新创建/加入房间以继续联机。 multiplayer.session.join=加入房间 +multiplayer.session.join.error=加入房间失败 multiplayer.session.join.hint=你需要向已经创建好房间的玩家索要邀请码以便加入多人联机房间 multiplayer.session.join.invitation_code=邀请码 multiplayer.session.join.invitation_code.error=邀请码不正确,请向开服玩家获取邀请码 diff --git a/HMCL/src/test/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClientServerTest.java b/HMCL/src/test/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClientServerTest.java index e4b602ddf..e1d4022f5 100644 --- a/HMCL/src/test/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClientServerTest.java +++ b/HMCL/src/test/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClientServerTest.java @@ -18,13 +18,13 @@ package org.jackhuang.hmcl.ui.multiplayer; import org.jackhuang.hmcl.util.Logging; -import org.junit.Assert; +import org.junit.Ignore; import org.junit.Test; public class MultiplayerClientServerTest { @Test -// @Ignore + @Ignore public void startServer() throws Exception { Logging.initForTest(); MultiplayerServer server = new MultiplayerServer(1000); @@ -33,8 +33,7 @@ public class MultiplayerClientServerTest { MultiplayerClient client = new MultiplayerClient("username", 44444); client.start(); - server.onClientAdded().register(event -> { - Assert.assertEquals("username", event.getUsername()); + server.onKeepAlive().register(event -> { client.interrupt(); server.interrupt(); });