feat(multiplayer): relay mode.
This commit is contained in:
@@ -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<String, Object> 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<String, Object> 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<String, JsonElement> entry : obj.entrySet()) {
|
||||
|
||||
@@ -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<HMCLAccount> account = new SimpleObjectProperty<>();
|
||||
|
||||
private HMCLAccounts() {
|
||||
}
|
||||
|
||||
public static HMCLAccount getAccount() {
|
||||
return account.get();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<CatoSession> joinSession(String token, String version, String sessionName, String peer, int remotePort, int localPort) throws IncompatibleCatoVersionException {
|
||||
public static CompletableFuture<CatoSession> 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 {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -265,6 +265,12 @@ public class MultiplayerPageSkin extends SkinBase<MultiplayerPage> {
|
||||
|
||||
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<MultiplayerPage> {
|
||||
placeholder,
|
||||
FXUtils.segmentToTextFlow(i18n("multiplayer.powered_by"), Controllers::onHyperlinkAction));
|
||||
|
||||
thanksPane.getContent().addAll(gridPane, pane);
|
||||
thanksPane.getContent().addAll(gridPane, relay, pane);
|
||||
}
|
||||
|
||||
content.getChildren().setAll(
|
||||
|
||||
@@ -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<CatoClient> onClientAdding;
|
||||
private final EventManager<MultiplayerChannel.CatoClient> onClientAdded = new EventManager<>();
|
||||
private final EventManager<MultiplayerChannel.CatoClient> onClientDisconnected = new EventManager<>();
|
||||
private final EventManager<Event> onKeepAlive = new EventManager<>();
|
||||
|
||||
private final Map<String, Client> clients = new ConcurrentHashMap<>();
|
||||
private final Map<String, Client> nameClientMap = new ConcurrentHashMap<>();
|
||||
private final Map<String, Endpoint> clients = new ConcurrentHashMap<>();
|
||||
private final Map<String, Endpoint> nameClientMap = new ConcurrentHashMap<>();
|
||||
|
||||
public MultiplayerServer(int gamePort) {
|
||||
this.gamePort = gamePort;
|
||||
@@ -63,6 +65,10 @@ public class MultiplayerServer extends Thread {
|
||||
return onClientDisconnected;
|
||||
}
|
||||
|
||||
public EventManager<Event> 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;
|
||||
}
|
||||
|
||||
@@ -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 <a href="https://noin.cn">noin.cn</a>. <a href="https://noin.cn/agreement">EULA</a>
|
||||
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.
|
||||
|
||||
@@ -624,6 +624,8 @@ multiplayer.nat.type.symmetric_udp_firewall=差(對稱型+防火牆)
|
||||
multiplayer.nat.type.unknown=未知
|
||||
multiplayer.powered_by=多人聯機服務由 這裡 (<a href="https://noin.cn">noin.cn</a>) 提供。<a href="https://noin.cn/agreement">用戶協議與免責聲明</a>
|
||||
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=邀請碼不正確,請向開服玩家獲取邀請碼
|
||||
|
||||
@@ -623,6 +623,8 @@ multiplayer.nat.type.symmetric=差(对称型)
|
||||
multiplayer.nat.type.symmetric_udp_firewall=差(对称型+防火墙)
|
||||
multiplayer.nat.type.unknown=未知
|
||||
multiplayer.powered_by=多人联机服务由 这里 (<a href="https://noin.cn">noin.cn</a>) 提供。<a href="https://noin.cn/agreement">用户协议与免责声明</a>
|
||||
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=邀请码不正确,请向开服玩家获取邀请码
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user