feat(multiplayer): relay mode.
This commit is contained in:
@@ -21,10 +21,7 @@ import com.google.gson.*;
|
|||||||
import com.google.gson.annotations.JsonAdapter;
|
import com.google.gson.annotations.JsonAdapter;
|
||||||
import javafx.beans.InvalidationListener;
|
import javafx.beans.InvalidationListener;
|
||||||
import javafx.beans.Observable;
|
import javafx.beans.Observable;
|
||||||
import javafx.beans.property.IntegerProperty;
|
import javafx.beans.property.*;
|
||||||
import javafx.beans.property.SimpleIntegerProperty;
|
|
||||||
import javafx.beans.property.SimpleStringProperty;
|
|
||||||
import javafx.beans.property.StringProperty;
|
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.collections.ObservableMap;
|
import javafx.collections.ObservableMap;
|
||||||
import javafx.collections.ObservableSet;
|
import javafx.collections.ObservableSet;
|
||||||
@@ -73,6 +70,8 @@ public class GlobalConfig implements Cloneable, Observable {
|
|||||||
|
|
||||||
private StringProperty multiplayerToken = new SimpleStringProperty();
|
private StringProperty multiplayerToken = new SimpleStringProperty();
|
||||||
|
|
||||||
|
private BooleanProperty multiplayerRelay = new SimpleBooleanProperty();
|
||||||
|
|
||||||
private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0);
|
private IntegerProperty multiplayerAgreementVersion = new SimpleIntegerProperty(0);
|
||||||
|
|
||||||
private final Map<String, Object> unknownFields = new HashMap<>();
|
private final Map<String, Object> unknownFields = new HashMap<>();
|
||||||
@@ -114,6 +113,18 @@ public class GlobalConfig implements Cloneable, Observable {
|
|||||||
this.agreementVersion.set(agreementVersion);
|
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() {
|
public int getMultiplayerAgreementVersion() {
|
||||||
return multiplayerAgreementVersion.get();
|
return multiplayerAgreementVersion.get();
|
||||||
}
|
}
|
||||||
@@ -154,6 +165,7 @@ public class GlobalConfig implements Cloneable, Observable {
|
|||||||
JsonObject jsonObject = new JsonObject();
|
JsonObject jsonObject = new JsonObject();
|
||||||
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
jsonObject.add("agreementVersion", context.serialize(src.getAgreementVersion()));
|
||||||
jsonObject.add("multiplayerToken", context.serialize(src.getMultiplayerToken()));
|
jsonObject.add("multiplayerToken", context.serialize(src.getMultiplayerToken()));
|
||||||
|
jsonObject.add("multiplayerRelay", context.serialize(src.isMultiplayerRelay()));
|
||||||
jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion()));
|
jsonObject.add("multiplayerAgreementVersion", context.serialize(src.getMultiplayerAgreementVersion()));
|
||||||
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
|
for (Map.Entry<String, Object> entry : src.unknownFields.entrySet()) {
|
||||||
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
|
jsonObject.add(entry.getKey(), context.serialize(entry.getValue()));
|
||||||
@@ -171,6 +183,7 @@ public class GlobalConfig implements Cloneable, Observable {
|
|||||||
GlobalConfig config = new GlobalConfig();
|
GlobalConfig config = new GlobalConfig();
|
||||||
config.setAgreementVersion(Optional.ofNullable(obj.get("agreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
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.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));
|
config.setMultiplayerAgreementVersion(Optional.ofNullable(obj.get("multiplayerAgreementVersion")).map(JsonElement::getAsInt).orElse(0));
|
||||||
|
|
||||||
for (Map.Entry<String, JsonElement> entry : obj.entrySet()) {
|
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.Lang.mapOf;
|
||||||
import static org.jackhuang.hmcl.util.Pair.pair;
|
import static org.jackhuang.hmcl.util.Pair.pair;
|
||||||
|
|
||||||
public class HMCLAccounts {
|
public final class HMCLAccounts {
|
||||||
|
|
||||||
private static final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
|
private static final ObjectProperty<HMCLAccount> account = new SimpleObjectProperty<>();
|
||||||
|
|
||||||
|
private HMCLAccounts() {
|
||||||
|
}
|
||||||
|
|
||||||
public static HMCLAccount getAccount() {
|
public static HMCLAccount getAccount() {
|
||||||
return account.get();
|
return account.get();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,13 @@ import javafx.geometry.Pos;
|
|||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import org.jackhuang.hmcl.ui.FXUtils;
|
import org.jackhuang.hmcl.ui.FXUtils;
|
||||||
|
import org.jackhuang.hmcl.util.StringUtils;
|
||||||
|
|
||||||
public class OptionToggleButton extends StackPane {
|
public class OptionToggleButton extends StackPane {
|
||||||
private final StringProperty title = new SimpleStringProperty();
|
private final StringProperty title = new SimpleStringProperty();
|
||||||
|
private final StringProperty subtitle = new SimpleStringProperty();
|
||||||
private final BooleanProperty selected = new SimpleBooleanProperty();
|
private final BooleanProperty selected = new SimpleBooleanProperty();
|
||||||
|
|
||||||
public OptionToggleButton() {
|
public OptionToggleButton() {
|
||||||
@@ -41,11 +44,15 @@ public class OptionToggleButton extends StackPane {
|
|||||||
RipplerContainer container = new RipplerContainer(pane);
|
RipplerContainer container = new RipplerContainer(pane);
|
||||||
getChildren().setAll(container);
|
getChildren().setAll(container);
|
||||||
|
|
||||||
Label label = new Label();
|
VBox left = new VBox();
|
||||||
label.setMouseTransparent(true);
|
Label titleLabel = new Label();
|
||||||
label.textProperty().bind(title);
|
titleLabel.setMouseTransparent(true);
|
||||||
pane.setLeft(label);
|
titleLabel.textProperty().bind(title);
|
||||||
BorderPane.setAlignment(label, Pos.CENTER_LEFT);
|
Label subtitleLabel = new Label();
|
||||||
|
subtitleLabel.setMouseTransparent(true);
|
||||||
|
subtitleLabel.textProperty().bind(subtitle);
|
||||||
|
pane.setLeft(left);
|
||||||
|
BorderPane.setAlignment(left, Pos.CENTER_LEFT);
|
||||||
|
|
||||||
JFXToggleButton toggleButton = new JFXToggleButton();
|
JFXToggleButton toggleButton = new JFXToggleButton();
|
||||||
pane.setRight(toggleButton);
|
pane.setRight(toggleButton);
|
||||||
@@ -56,6 +63,14 @@ public class OptionToggleButton extends StackPane {
|
|||||||
container.setOnMouseClicked(e -> {
|
container.setOnMouseClicked(e -> {
|
||||||
toggleButton.setSelected(!toggleButton.isSelected());
|
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() {
|
public String getTitle() {
|
||||||
@@ -70,6 +85,18 @@ public class OptionToggleButton extends StackPane {
|
|||||||
this.title.set(title);
|
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() {
|
public boolean isSelected() {
|
||||||
return selected.get();
|
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));
|
HttpRequest req = HttpRequest.GET(NetworkUtils.withQuery("https://hmcl.huangyuhui.net/api/feedback", query));
|
||||||
if (account != null) {
|
if (account != null) {
|
||||||
req.authorization("Bearer", HMCLAccounts.getAccount().getIdToken())
|
req.authorization("Bearer", HMCLAccounts.getAccount().getIdToken())
|
||||||
.header("Authorization-Provider", HMCLAccounts.getAccount().getProvider());
|
.header("Authorization-Provider", HMCLAccounts.getAccount().getProvider());
|
||||||
}
|
}
|
||||||
return req.<List<FeedbackResponse>>getJson(new TypeToken<List<FeedbackResponse>>(){}.getType());
|
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.List;
|
||||||
import java.util.stream.Collectors;
|
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.FXUtils.runInFX;
|
||||||
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap;
|
||||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||||
|
|||||||
@@ -73,19 +73,22 @@ public class MultiplayerClient extends Thread {
|
|||||||
public void run() {
|
public void run() {
|
||||||
LOG.info("Connecting to 127.0.0.1:" + port);
|
LOG.info("Connecting to 127.0.0.1:" + port);
|
||||||
for (int i = 0; i < 5; i++) {
|
for (int i = 0; i < 5; i++) {
|
||||||
|
KeepAliveThread keepAliveThread = null;
|
||||||
try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
|
try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port);
|
||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
|
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);
|
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.write(JsonUtils.UGLY_GSON.toJson(new JoinRequest(MultiplayerManager.CATO_VERSION, id)));
|
||||||
writer.newLine();
|
writer.newLine();
|
||||||
writer.flush();
|
writer.flush();
|
||||||
|
|
||||||
LOG.fine("Sent join request with id=" + id);
|
LOG.fine("Sent join request with id=" + id);
|
||||||
|
|
||||||
|
keepAliveThread = new KeepAliveThread(endpoint);
|
||||||
|
keepAliveThread.start();
|
||||||
|
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
if (isInterrupted()) {
|
if (isInterrupted()) {
|
||||||
@@ -106,6 +109,7 @@ public class MultiplayerClient extends Thread {
|
|||||||
onKicked.fireEvent(new Event(this));
|
onKicked.fireEvent(new Event(this));
|
||||||
|
|
||||||
LOG.fine("Kicked by the server");
|
LOG.fine("Kicked by the server");
|
||||||
|
} else if (response instanceof KeepAliveResponse) {
|
||||||
} else {
|
} else {
|
||||||
LOG.log(Level.WARNING, "Unrecognized packet from server:" + line);
|
LOG.log(Level.WARNING, "Unrecognized packet from server:" + line);
|
||||||
}
|
}
|
||||||
@@ -121,12 +125,42 @@ public class MultiplayerClient extends Thread {
|
|||||||
continue;
|
continue;
|
||||||
} catch (IOException | JsonParseException e) {
|
} catch (IOException | JsonParseException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
if (keepAliveThread != null) {
|
||||||
|
keepAliveThread.interrupt();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
LOG.info("Lost connection to 127.0.0.1:" + port);
|
LOG.info("Lost connection to 127.0.0.1:" + port);
|
||||||
onDisconnected.fireEvent(new Event(this));
|
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 {
|
public static class ConnectedEvent extends Event {
|
||||||
private final int port;
|
private final int port;
|
||||||
|
|
||||||
|
|||||||
@@ -44,10 +44,7 @@ import java.nio.charset.StandardCharsets;
|
|||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.attribute.PosixFilePermission;
|
import java.nio.file.attribute.PosixFilePermission;
|
||||||
import java.util.Arrays;
|
import java.util.*;
|
||||||
import java.util.Base64;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
import java.util.concurrent.CancellationException;
|
import java.util.concurrent.CancellationException;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.logging.Level;
|
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 REMOTE_ADDRESS = "127.0.0.1";
|
||||||
private static final String LOCAL_ADDRESS = "0.0.0.0";
|
private static final String LOCAL_ADDRESS = "0.0.0.0";
|
||||||
private static final String MODE = "p2p";
|
|
||||||
|
|
||||||
private MultiplayerManager() {
|
private MultiplayerManager() {
|
||||||
}
|
}
|
||||||
@@ -89,7 +85,7 @@ public final class MultiplayerManager {
|
|||||||
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH);
|
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)) {
|
if (!CATO_VERSION.equals(version)) {
|
||||||
throw new IncompatibleCatoVersionException(version, CATO_VERSION);
|
throw new IncompatibleCatoVersionException(version, CATO_VERSION);
|
||||||
}
|
}
|
||||||
@@ -100,12 +96,16 @@ public final class MultiplayerManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return CompletableFuture.completedFuture(null).thenComposeAsync(unused -> {
|
return CompletableFuture.completedFuture(null).thenComposeAsync(unused -> {
|
||||||
|
if (!isPortAvailable(3478)) {
|
||||||
|
throw new CatoAlreadyStartedException();
|
||||||
|
}
|
||||||
|
|
||||||
String[] commands = new String[]{exe.toString(),
|
String[] commands = new String[]{exe.toString(),
|
||||||
"--token", StringUtils.isBlank(token) ? "new" : token,
|
"--token", StringUtils.isBlank(token) ? "new" : token,
|
||||||
"--id", peer,
|
"--id", peer,
|
||||||
"--local", String.format("%s:%d", LOCAL_ADDRESS, localPort),
|
"--local", String.format("%s:%d", LOCAL_ADDRESS, localPort),
|
||||||
"--remote", String.format("%s:%d", REMOTE_ADDRESS, remotePort),
|
"--remote", String.format("%s:%d", REMOTE_ADDRESS, remotePort),
|
||||||
"--mode", MODE};
|
"--mode", mode.getName()};
|
||||||
Process process;
|
Process process;
|
||||||
try {
|
try {
|
||||||
process = new ProcessBuilder()
|
process = new ProcessBuilder()
|
||||||
@@ -137,18 +137,21 @@ public final class MultiplayerManager {
|
|||||||
client.onConnected().register(connectedEvent -> {
|
client.onConnected().register(connectedEvent -> {
|
||||||
try {
|
try {
|
||||||
int port = findAvailablePort();
|
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);
|
LOG.info("Invoking cato: " + command);
|
||||||
|
client.setGamePort(port);
|
||||||
writer.write(command);
|
writer.write(command);
|
||||||
writer.newLine();
|
writer.newLine();
|
||||||
writer.flush();
|
writer.flush();
|
||||||
future.complete(session);
|
future.complete(session);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
future.completeExceptionally(e);
|
future.completeExceptionally(e);
|
||||||
|
session.stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
client.onKicked().register(kickedEvent -> {
|
client.onKicked().register(kickedEvent -> {
|
||||||
future.completeExceptionally(new CancellationException());
|
future.completeExceptionally(new CancellationException());
|
||||||
|
session.stop();
|
||||||
});
|
});
|
||||||
client.start();
|
client.start();
|
||||||
});
|
});
|
||||||
@@ -163,13 +166,16 @@ public final class MultiplayerManager {
|
|||||||
throw new IllegalStateException("Cato file not found");
|
throw new IllegalStateException("Cato file not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isPortAvailable(3478)) {
|
||||||
|
throw new CatoAlreadyStartedException();
|
||||||
|
}
|
||||||
|
|
||||||
MultiplayerServer server = new MultiplayerServer(gamePort);
|
MultiplayerServer server = new MultiplayerServer(gamePort);
|
||||||
server.startServer();
|
server.startServer();
|
||||||
|
|
||||||
String[] commands = new String[]{exe.toString(),
|
String[] commands = new String[]{exe.toString(),
|
||||||
"--token", StringUtils.isBlank(token) ? "new" : token,
|
"--token", StringUtils.isBlank(token) ? "new" : token,
|
||||||
"--allows", String.format("%s:%d/%s:%d", REMOTE_ADDRESS, server.getPort(), REMOTE_ADDRESS, gamePort),
|
"--allows", String.format("%s:%d/%s:%d", REMOTE_ADDRESS, server.getPort(), REMOTE_ADDRESS, gamePort)};
|
||||||
"--mode", MODE};
|
|
||||||
Process process = new ProcessBuilder()
|
Process process = new ProcessBuilder()
|
||||||
.command(commands)
|
.command(commands)
|
||||||
.start();
|
.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() {
|
public static String getCatoPath() {
|
||||||
switch (OperatingSystem.CURRENT_OS) {
|
switch (OperatingSystem.CURRENT_OS) {
|
||||||
case WINDOWS:
|
case WINDOWS:
|
||||||
@@ -420,4 +434,16 @@ public final class MultiplayerManager {
|
|||||||
return actual;
|
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);
|
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) {
|
} catch (Exception e) {
|
||||||
LOG.log(Level.WARNING, "Failed to create session", e);
|
LOG.log(Level.WARNING, "Failed to create session", e);
|
||||||
reject.accept(i18n("multiplayer.session.create.error"));
|
reject.accept(i18n("multiplayer.session.create.error"));
|
||||||
@@ -264,7 +267,16 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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 -> {
|
.thenAcceptAsync(session -> {
|
||||||
initCatoSession(session);
|
initCatoSession(session);
|
||||||
|
|
||||||
@@ -295,9 +307,13 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
|||||||
LOG.info("Connection rejected by the server");
|
LOG.info("Connection rejected by the server");
|
||||||
reject.accept(i18n("multiplayer.session.join.rejected"));
|
reject.accept(i18n("multiplayer.session.join.rejected"));
|
||||||
return null;
|
return null;
|
||||||
|
} else if (throwable instanceof MultiplayerManager.CatoAlreadyStartedException) {
|
||||||
|
LOG.info("Cato already started");
|
||||||
|
reject.accept(i18n("multiplayer.session.error.already_started"));
|
||||||
|
return null;
|
||||||
} else {
|
} else {
|
||||||
LOG.log(Level.WARNING, "Failed to join sessoin");
|
LOG.log(Level.WARNING, "Failed to join sessoin");
|
||||||
reject.accept(i18n("multiplayer.session.error"));
|
reject.accept(i18n("multiplayer.session.join.error"));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -265,6 +265,12 @@ public class MultiplayerPageSkin extends SkinBase<MultiplayerPage> {
|
|||||||
|
|
||||||
gridPane.addRow(0, new Label(i18n("multiplayer.session.create.token")), tokenField, applyLink);
|
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();
|
HBox pane = new HBox();
|
||||||
pane.setAlignment(Pos.CENTER_LEFT);
|
pane.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
@@ -280,7 +286,7 @@ public class MultiplayerPageSkin extends SkinBase<MultiplayerPage> {
|
|||||||
placeholder,
|
placeholder,
|
||||||
FXUtils.segmentToTextFlow(i18n("multiplayer.powered_by"), Controllers::onHyperlinkAction));
|
FXUtils.segmentToTextFlow(i18n("multiplayer.powered_by"), Controllers::onHyperlinkAction));
|
||||||
|
|
||||||
thanksPane.getContent().addAll(gridPane, pane);
|
thanksPane.getContent().addAll(gridPane, relay, pane);
|
||||||
}
|
}
|
||||||
|
|
||||||
content.getChildren().setAll(
|
content.getChildren().setAll(
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
package org.jackhuang.hmcl.ui.multiplayer;
|
package org.jackhuang.hmcl.ui.multiplayer;
|
||||||
|
|
||||||
import com.google.gson.JsonParseException;
|
import com.google.gson.JsonParseException;
|
||||||
|
import org.jackhuang.hmcl.event.Event;
|
||||||
import org.jackhuang.hmcl.event.EventManager;
|
import org.jackhuang.hmcl.event.EventManager;
|
||||||
import org.jackhuang.hmcl.util.FutureCallback;
|
import org.jackhuang.hmcl.util.FutureCallback;
|
||||||
import org.jackhuang.hmcl.util.Lang;
|
import org.jackhuang.hmcl.util.Lang;
|
||||||
@@ -40,9 +41,10 @@ public class MultiplayerServer extends Thread {
|
|||||||
private FutureCallback<CatoClient> onClientAdding;
|
private FutureCallback<CatoClient> onClientAdding;
|
||||||
private final EventManager<MultiplayerChannel.CatoClient> onClientAdded = new EventManager<>();
|
private final EventManager<MultiplayerChannel.CatoClient> onClientAdded = new EventManager<>();
|
||||||
private final EventManager<MultiplayerChannel.CatoClient> onClientDisconnected = 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, Endpoint> clients = new ConcurrentHashMap<>();
|
||||||
private final Map<String, Client> nameClientMap = new ConcurrentHashMap<>();
|
private final Map<String, Endpoint> nameClientMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public MultiplayerServer(int gamePort) {
|
public MultiplayerServer(int gamePort) {
|
||||||
this.gamePort = gamePort;
|
this.gamePort = gamePort;
|
||||||
@@ -63,6 +65,10 @@ public class MultiplayerServer extends Thread {
|
|||||||
return onClientDisconnected;
|
return onClientDisconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EventManager<Event> onKeepAlive() {
|
||||||
|
return onKeepAlive;
|
||||||
|
}
|
||||||
|
|
||||||
public void startServer() throws IOException {
|
public void startServer() throws IOException {
|
||||||
startServer(0);
|
startServer(0);
|
||||||
}
|
}
|
||||||
@@ -98,7 +104,7 @@ public class MultiplayerServer extends Thread {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void kickPlayer(CatoClient player) {
|
public void kickPlayer(CatoClient player) {
|
||||||
Client client = nameClientMap.get(player.getUsername());
|
Endpoint client = nameClientMap.get(player.getUsername());
|
||||||
if (client == null) return;
|
if (client == null) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -119,8 +125,8 @@ public class MultiplayerServer extends Thread {
|
|||||||
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
|
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()))) {
|
||||||
clientSocket.setKeepAlive(true);
|
clientSocket.setKeepAlive(true);
|
||||||
Client client = new Client(clientSocket, writer);
|
Endpoint endpoint = new Endpoint(clientSocket, writer);
|
||||||
clients.put(address, client);
|
clients.put(address, endpoint);
|
||||||
|
|
||||||
String line;
|
String line;
|
||||||
while ((line = reader.readLine()) != null) {
|
while ((line = reader.readLine()) != null) {
|
||||||
@@ -137,13 +143,13 @@ public class MultiplayerServer extends Thread {
|
|||||||
clientName = joinRequest.getUsername();
|
clientName = joinRequest.getUsername();
|
||||||
|
|
||||||
CatoClient catoClient = new CatoClient(this, clientName);
|
CatoClient catoClient = new CatoClient(this, clientName);
|
||||||
nameClientMap.put(clientName, client);
|
nameClientMap.put(clientName, endpoint);
|
||||||
onClientAdded.fireEvent(catoClient);
|
onClientAdded.fireEvent(catoClient);
|
||||||
|
|
||||||
if (onClientAdding != null) {
|
if (onClientAdding != null) {
|
||||||
onClientAdding.call(catoClient, () -> {
|
onClientAdding.call(catoClient, () -> {
|
||||||
try {
|
try {
|
||||||
client.write(new JoinResponse(gamePort));
|
endpoint.write(new JoinResponse(gamePort));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
LOG.log(Level.WARNING, "Failed to send join response.", e);
|
LOG.log(Level.WARNING, "Failed to send join response.", e);
|
||||||
try {
|
try {
|
||||||
@@ -155,7 +161,7 @@ public class MultiplayerServer extends Thread {
|
|||||||
}
|
}
|
||||||
}, msg -> {
|
}, msg -> {
|
||||||
try {
|
try {
|
||||||
client.write(new KickResponse(msg));
|
endpoint.write(new KickResponse(msg));
|
||||||
LOG.info("Rejected join request from id=" + joinRequest.getUsername());
|
LOG.info("Rejected join request from id=" + joinRequest.getUsername());
|
||||||
socket.close();
|
socket.close();
|
||||||
} catch (IOException e) {
|
} 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 {
|
} else {
|
||||||
LOG.log(Level.WARNING, "Unrecognized packet from client " + targetSocket.getRemoteSocketAddress() + ":" + line);
|
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 Socket socket;
|
||||||
public final BufferedWriter writer;
|
public final BufferedWriter writer;
|
||||||
|
|
||||||
public Client(Socket socket, BufferedWriter writer) {
|
public Endpoint(Socket socket, BufferedWriter writer) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.writer = writer;
|
this.writer = writer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -624,15 +624,17 @@ multiplayer.nat.type.symmetric_udp_firewall=Bad (Symmetric with UDP Firewall)
|
|||||||
multiplayer.nat.type.unknown=Unknown
|
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.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.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=Room
|
||||||
multiplayer.session.name.format=%1$s's Room
|
multiplayer.session.name.format=%1$s's Room
|
||||||
multiplayer.session.name.motd=HMCL Multiplayer Session - %s
|
multiplayer.session.name.motd=HMCL Multiplayer Session - %s
|
||||||
multiplayer.session.close=Close Room
|
multiplayer.session.close=Close Room
|
||||||
multiplayer.session.close.warning=After closing room, all players joined the room will lost connection. Continue?
|
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.copy_room_code=Copy Invitation Code
|
||||||
multiplayer.session.create=Create Room
|
multiplayer.session.create=Create Session
|
||||||
multiplayer.session.create.error=Failed to create multiplayer room.
|
multiplayer.session.create.error=Failed to create multiplayer session.
|
||||||
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.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=Connection request
|
||||||
multiplayer.session.create.join.prompt=Player %s wants to join the multiplayer session. Accept?
|
multiplayer.session.create.join.prompt=Player %s wants to join the multiplayer session. Accept?
|
||||||
multiplayer.session.create.members=Members
|
multiplayer.session.create.members=Members
|
||||||
@@ -642,14 +644,16 @@ multiplayer.session.create.name=Session Name
|
|||||||
multiplayer.session.create.port=Port
|
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.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=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.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.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.hint=You must click "Open LAN Server" in game in order to enable multiplayer functionality.
|
||||||
multiplayer.session.join=Join Room
|
multiplayer.session.join=Join Session
|
||||||
multiplayer.session.join.hint=You must obtain the invitation code from the gamer who has already created a multiplayer room.
|
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=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.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.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.
|
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.nat.type.unknown=未知
|
||||||
multiplayer.powered_by=多人聯機服務由 這裡 (<a href="https://noin.cn">noin.cn</a>) 提供。<a href="https://noin.cn/agreement">用戶協議與免責聲明</a>
|
multiplayer.powered_by=多人聯機服務由 這裡 (<a href="https://noin.cn">noin.cn</a>) 提供。<a href="https://noin.cn/agreement">用戶協議與免責聲明</a>
|
||||||
multiplayer.report=違法違規檢舉
|
multiplayer.report=違法違規檢舉
|
||||||
|
multiplayer.relay=中繼模式
|
||||||
|
multiplayer.relay.hint=NAT 網路類型為差-對稱型的用戶,若無法聯機,申請靜態 Token 後可以啟用本選項
|
||||||
multiplayer.session=房間
|
multiplayer.session=房間
|
||||||
multiplayer.session.name.format=%1$s 的房間
|
multiplayer.session.name.format=%1$s 的房間
|
||||||
multiplayer.session.name.motd=HMCL 多人聯機房間 - %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=Token
|
||||||
multiplayer.session.create.token.apply=申請靜態 Token
|
multiplayer.session.create.token.apply=申請靜態 Token
|
||||||
multiplayer.session.create.token.prompt=預設為臨時 Token。你可以在 noin.cn 上申請靜態 Token 並填寫至此處
|
multiplayer.session.create.token.prompt=預設為臨時 Token。你可以在 noin.cn 上申請靜態 Token 並填寫至此處
|
||||||
|
multiplayer.session.error.already_started=本地已經開啟 cato 服務,請檢查是否有其他 HMCL 正在運行聯機服務。或者你可以在任務管理器裡殺掉 cato 進程以繼續。
|
||||||
multiplayer.session.expired=聯機會話連續使用時間超過了 3 小時,你需要重新創建/加入房間以繼續聯機。
|
multiplayer.session.expired=聯機會話連續使用時間超過了 3 小時,你需要重新創建/加入房間以繼續聯機。
|
||||||
multiplayer.session.join=加入房間
|
multiplayer.session.join=加入房間
|
||||||
|
multiplayer.session.join.error=加入房間失敗
|
||||||
multiplayer.session.join.hint=你需要向已經創建好房間的玩家索要邀請碼以便加入多人聯機房間
|
multiplayer.session.join.hint=你需要向已經創建好房間的玩家索要邀請碼以便加入多人聯機房間
|
||||||
multiplayer.session.join.invitation_code=邀請碼
|
multiplayer.session.join.invitation_code=邀請碼
|
||||||
multiplayer.session.join.invitation_code.error=邀請碼不正確,請向開服玩家獲取邀請碼
|
multiplayer.session.join.invitation_code.error=邀請碼不正確,請向開服玩家獲取邀請碼
|
||||||
|
|||||||
@@ -623,6 +623,8 @@ multiplayer.nat.type.symmetric=差(对称型)
|
|||||||
multiplayer.nat.type.symmetric_udp_firewall=差(对称型+防火墙)
|
multiplayer.nat.type.symmetric_udp_firewall=差(对称型+防火墙)
|
||||||
multiplayer.nat.type.unknown=未知
|
multiplayer.nat.type.unknown=未知
|
||||||
multiplayer.powered_by=多人联机服务由 这里 (<a href="https://noin.cn">noin.cn</a>) 提供。<a href="https://noin.cn/agreement">用户协议与免责声明</a>
|
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.report=违法违规举报
|
||||||
multiplayer.session=房间
|
multiplayer.session=房间
|
||||||
multiplayer.session.name.format=%1$s 的房间
|
multiplayer.session.name.format=%1$s 的房间
|
||||||
@@ -644,8 +646,10 @@ multiplayer.session.create.port.error=无法检测游戏端口号,你必须先
|
|||||||
multiplayer.session.create.token=Token
|
multiplayer.session.create.token=Token
|
||||||
multiplayer.session.create.token.apply=申请静态 Token
|
multiplayer.session.create.token.apply=申请静态 Token
|
||||||
multiplayer.session.create.token.prompt=默认为临时 Token。你可以在 noin.cn 上申请静态 Token 并填写至此处
|
multiplayer.session.create.token.prompt=默认为临时 Token。你可以在 noin.cn 上申请静态 Token 并填写至此处
|
||||||
|
multiplayer.session.error.already_started=本地已经开启 cato 服务,请检查是否有其他 HMCL 正在运行联机服务。或者你可以在任务管理器里杀掉 cato 进程以继续。
|
||||||
multiplayer.session.expired=联机会话连续使用时间超过了 3 小时,你需要重新创建/加入房间以继续联机。
|
multiplayer.session.expired=联机会话连续使用时间超过了 3 小时,你需要重新创建/加入房间以继续联机。
|
||||||
multiplayer.session.join=加入房间
|
multiplayer.session.join=加入房间
|
||||||
|
multiplayer.session.join.error=加入房间失败
|
||||||
multiplayer.session.join.hint=你需要向已经创建好房间的玩家索要邀请码以便加入多人联机房间
|
multiplayer.session.join.hint=你需要向已经创建好房间的玩家索要邀请码以便加入多人联机房间
|
||||||
multiplayer.session.join.invitation_code=邀请码
|
multiplayer.session.join.invitation_code=邀请码
|
||||||
multiplayer.session.join.invitation_code.error=邀请码不正确,请向开服玩家获取邀请码
|
multiplayer.session.join.invitation_code.error=邀请码不正确,请向开服玩家获取邀请码
|
||||||
|
|||||||
@@ -18,13 +18,13 @@
|
|||||||
package org.jackhuang.hmcl.ui.multiplayer;
|
package org.jackhuang.hmcl.ui.multiplayer;
|
||||||
|
|
||||||
import org.jackhuang.hmcl.util.Logging;
|
import org.jackhuang.hmcl.util.Logging;
|
||||||
import org.junit.Assert;
|
import org.junit.Ignore;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
public class MultiplayerClientServerTest {
|
public class MultiplayerClientServerTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
// @Ignore
|
@Ignore
|
||||||
public void startServer() throws Exception {
|
public void startServer() throws Exception {
|
||||||
Logging.initForTest();
|
Logging.initForTest();
|
||||||
MultiplayerServer server = new MultiplayerServer(1000);
|
MultiplayerServer server = new MultiplayerServer(1000);
|
||||||
@@ -33,8 +33,7 @@ public class MultiplayerClientServerTest {
|
|||||||
MultiplayerClient client = new MultiplayerClient("username", 44444);
|
MultiplayerClient client = new MultiplayerClient("username", 44444);
|
||||||
client.start();
|
client.start();
|
||||||
|
|
||||||
server.onClientAdded().register(event -> {
|
server.onKeepAlive().register(event -> {
|
||||||
Assert.assertEquals("username", event.getUsername());
|
|
||||||
client.interrupt();
|
client.interrupt();
|
||||||
server.interrupt();
|
server.interrupt();
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user