feat(multiplayer): much simpler invitation code format.

This commit is contained in:
huanghongxun 2021-10-20 00:25:45 +08:00
parent 1762e78cb3
commit de421f93b5
9 changed files with 104 additions and 60 deletions

View File

@ -87,12 +87,18 @@ public final class MultiplayerChannel {
}
public static class JoinResponse extends Response {
private final String sessionName;
private final int port;
public JoinResponse(int port) {
public JoinResponse(String sessionName, int port) {
this.sessionName = sessionName;
this.port = port;
}
public String getSessionName() {
return sessionName;
}
public int getPort() {
return port;
}
@ -120,6 +126,10 @@ public final class MultiplayerChannel {
public String getMsg() {
return msg;
}
public static final String VERSION_NOT_MATCHED = "version_not_matched";
public static final String KICKED = "kicked";
public static final String JOIN_ACEEPTANCE_TIMEOUT = "join_acceptance_timeout";
}
public static class CatoClient extends Event {

View File

@ -43,7 +43,7 @@ public class MultiplayerClient extends Thread {
private final EventManager<ConnectedEvent> onConnected = new EventManager<>();
private final EventManager<Event> onDisconnected = new EventManager<>();
private final EventManager<Event> onKicked = new EventManager<>();
private final EventManager<KickEvent> onKicked = new EventManager<>();
private final EventManager<Event> onHandshake = new EventManager<>();
public MultiplayerClient(String id, int port) {
@ -70,7 +70,7 @@ public class MultiplayerClient extends Thread {
return onDisconnected;
}
public EventManager<Event> onKicked() {
public EventManager<KickEvent> onKicked() {
return onKicked;
}
@ -125,12 +125,12 @@ public class MultiplayerClient extends Thread {
connected = true;
onConnected.fireEvent(new ConnectedEvent(this, joinResponse.getPort()));
onConnected.fireEvent(new ConnectedEvent(this, joinResponse.getSessionName(), joinResponse.getPort()));
LOG.fine("Received join response with port " + joinResponse.getPort());
} else if (response instanceof KickResponse) {
LOG.fine("Kicked by the server");
onKicked.fireEvent(new Event(this));
onKicked.fireEvent(new KickEvent(this, ((KickResponse) response).getMsg()));
return;
} else if (response instanceof KeepAliveResponse) {
} else if (response instanceof HandshakeResponse) {
@ -193,15 +193,34 @@ public class MultiplayerClient extends Thread {
}
public static class ConnectedEvent extends Event {
private final String sessionName;
private final int port;
public ConnectedEvent(Object source, int port) {
public ConnectedEvent(Object source, String sessionName, int port) {
super(source);
this.sessionName = sessionName;
this.port = port;
}
public String getSessionName() {
return sessionName;
}
public int getPort() {
return port;
}
}
public static class KickEvent extends Event {
private final String reason;
public KickEvent(Object source, String reason) {
super(source);
this.reason = reason;
}
public String getReason() {
return reason;
}
}
}

View File

@ -27,7 +27,6 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.NetworkUtils;
import org.jackhuang.hmcl.util.platform.Architecture;
import org.jackhuang.hmcl.util.platform.CommandBuilder;
@ -87,7 +86,7 @@ public final class MultiplayerManager {
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH);
}
private static CompletableFuture<CatoSession> startCato(String sessionName, String token, State state) {
private static CompletableFuture<CatoSession> startCato(String token, State state) {
return CompletableFuture.completedFuture(null).thenApplyAsync(unused -> {
Path exe = getCatoExecutable();
if (!Files.isRegularFile(exe)) {
@ -108,18 +107,14 @@ public final class MultiplayerManager {
throw new UncheckedIOException(e);
}
return new CatoSession(sessionName, state, process, Arrays.asList(commands));
return new CatoSession(state, process, Arrays.asList(commands));
});
}
public static CompletableFuture<CatoSession> joinSession(String token, String version, String sessionName, String peer, Mode mode, int remotePort, int localPort, JoinSessionHandler handler) throws IncompatibleCatoVersionException {
if (!CATO_VERSION.equals(version)) {
throw new IncompatibleCatoVersionException(version, CATO_VERSION);
}
public static CompletableFuture<CatoSession> joinSession(String token, String peer, Mode mode, int remotePort, int localPort, JoinSessionHandler handler) throws IncompatibleCatoVersionException {
LOG.info(String.format("Joining session (token=%s,peer=%s,mode=%s,remotePort=%d,localPort=%d)", token, peer, mode, remotePort, localPort));
LOG.info(String.format("Joining session (token=%s,version=%s,sessionName=%s,peer=%s,mode=%s,remotePort=%d,localPort=%d)", token, version, sessionName, peer, mode, remotePort, localPort));
return startCato(sessionName, token, State.SLAVE).thenComposeAsync(wrap(session -> {
return startCato(token, State.SLAVE).thenComposeAsync(wrap(session -> {
CompletableFuture<CatoSession> future = new CompletableFuture<>();
session.forwardPort(peer, LOCAL_ADDRESS, localPort, REMOTE_ADDRESS, remotePort, mode);
@ -159,6 +154,7 @@ public final class MultiplayerManager {
int port = findAvailablePort();
session.forwardPort(peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort(), mode);
session.addRelatedThread(Lang.thread(new LocalServerBroadcaster(port, session), "LocalServerBroadcaster", true));
session.setName(connectedEvent.getSessionName());
client.setGamePort(port);
session.onExit.unregister(onExit);
future.complete(session);
@ -194,10 +190,10 @@ public final class MultiplayerManager {
public static CompletableFuture<CatoSession> createSession(String token, String sessionName, int gamePort, boolean allowAllJoinRequests) {
LOG.info(String.format("Creating session (token=%s,sessionName=%s,gamePort=%d)", token, sessionName, gamePort));
return startCato(sessionName, token, State.MASTER).thenComposeAsync(wrap(session -> {
return startCato(token, State.MASTER).thenComposeAsync(wrap(session -> {
CompletableFuture<CatoSession> future = new CompletableFuture<>();
MultiplayerServer server = new MultiplayerServer(gamePort, allowAllJoinRequests);
MultiplayerServer server = new MultiplayerServer(sessionName, gamePort, allowAllJoinRequests);
server.startServer();
session.allowForwardingAddress(REMOTE_ADDRESS, server.getPort());
@ -235,9 +231,12 @@ public final class MultiplayerManager {
}));
}
public static final Pattern INVITATION_CODE_PATTERN = Pattern.compile("^(?<id>(idx|mix)(.*?))#(?<port>\\d{2,5})$");
public static Invitation parseInvitationCode(String invitationCode) throws JsonParseException {
String json = new String(Base64.getDecoder().decode(invitationCode), StandardCharsets.UTF_8);
return JsonUtils.fromNonNullJson(json, Invitation.class);
Matcher matcher = INVITATION_CODE_PATTERN.matcher(invitationCode);
if (!matcher.find()) throw new IllegalArgumentException("Invalid invitation code");
return new Invitation(matcher.group("id"), Integer.parseInt(matcher.group("port")));
}
public static int findAvailablePort() throws IOException {
@ -300,7 +299,7 @@ public final class MultiplayerManager {
private final EventManager<CatoIdEvent> onIdGenerated = new EventManager<>();
private final EventManager<Event> onPeerConnected = new EventManager<>();
private final String name;
private String name;
private final State type;
private String id;
private boolean peerConnected = false;
@ -308,14 +307,13 @@ public final class MultiplayerManager {
private MultiplayerServer server;
private final BufferedWriter writer;
CatoSession(String name, State type, Process process, List<String> commands) {
CatoSession(State type, Process process, List<String> commands) {
super(process, commands);
Runtime.getRuntime().addShutdownHook(new Thread(this::stop));
LOG.info("Started cato with command: " + new CommandBuilder().addAll(commands).toString());
LOG.info("Started cato with command: " + new CommandBuilder().addAll(commands));
this.name = name;
this.type = type;
addRelatedThread(Lang.thread(this::waitFor, "CatoExitWaiter", true));
addRelatedThread(Lang.thread(new StreamPump(process.getInputStream(), this::checkCatoLog), "CatoInputStreamPump", true));
@ -386,6 +384,10 @@ public final class MultiplayerManager {
return name;
}
public void setName(String name) {
this.name = name;
}
public State getType() {
return type;
}
@ -399,8 +401,7 @@ public final class MultiplayerManager {
if (id == null) {
throw new IllegalStateException("id not generated");
}
String json = JsonUtils.UGLY_GSON.toJson(new Invitation(CATO_VERSION, id, name, serverPort));
return Base64.getEncoder().encodeToString(json.getBytes(StandardCharsets.UTF_8));
return id + "#" + serverPort;
}
public synchronized void invokeCommand(String command) throws IOException {
@ -476,33 +477,19 @@ public final class MultiplayerManager {
}
public static class Invitation {
@SerializedName("v")
private final String version;
private final String id;
@SerializedName("n")
private final String sessionName;
@SerializedName("p")
private final int channelPort;
public Invitation(String version, String id, String sessionName, int channelPort) {
this.version = version;
public Invitation(String id, int channelPort) {
this.id = id;
this.sessionName = sessionName;
this.channelPort = channelPort;
}
public String getVersion() {
return version;
}
public String getId() {
return id;
}
public String getSessionName() {
return sessionName;
}
public int getChannelPort() {
return channelPort;
}

View File

@ -48,6 +48,7 @@ import java.util.logging.Level;
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.KickResponse.*;
import static org.jackhuang.hmcl.util.Lang.resolveException;
import static org.jackhuang.hmcl.util.Logging.LOG;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@ -220,7 +221,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
session.getServer().setOnClientAdding((client, resolveClient, rejectClient) -> {
runInFX(() -> {
Controllers.dialog(new MessageDialogPane.Builder(i18n("multiplayer.session.create.join.prompt", client.getUsername()), i18n("multiplayer.session.create.join"), MessageDialogPane.MessageType.INFO)
.yesOrNo(resolveClient, () -> rejectClient.accept(i18n("multiplayer.session.join.wait_timeout")))
.yesOrNo(resolveClient, () -> rejectClient.accept(MultiplayerChannel.KickResponse.JOIN_ACEEPTANCE_TIMEOUT))
.cancelOnTimeout(30 * 1000)
.build());
});
@ -278,8 +279,6 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
try {
MultiplayerManager.joinSession(
globalConfig().getMultiplayerToken(),
invitation.getVersion(),
invitation.getSessionName(),
invitation.getId(),
globalConfig().isMultiplayerRelay() && (StringUtils.isNotBlank(globalConfig().getMultiplayerToken()) || StringUtils.isNotBlank(System.getProperty("hmcl.multiplayer.relay")))
? MultiplayerManager.Mode.BRIDGE
@ -307,10 +306,10 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
});
});
session.getClient().onKicked().register(() -> {
session.getClient().onKicked().register(kickedEvent -> {
runInFX(() -> {
kicked.set(true);
Controllers.dialog(i18n("multiplayer.session.join.kicked"));
Controllers.dialog(i18n("multiplayer.session.join.kicked", localizeKickMessage(kickedEvent.getReason())));
});
});
@ -330,6 +329,18 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.join.invitation_code"), "", new RequiredValidator())));
}
private String localizeKickMessage(String message) {
if (VERSION_NOT_MATCHED.equals(message)) {
return i18n("multiplayer.session.join.kicked.version_not_matched");
} else if (KICKED.equals(message)) {
return i18n("multiplayer.session.join.kicked.kicked");
} else if (JOIN_ACEEPTANCE_TIMEOUT.equals(message)) {
return i18n("multiplayer.session.join.kicked.join_acceptance_timeout");
} else {
return message;
}
}
private String localizeErrorMessage(Throwable t, boolean isStaticToken, Function<Throwable, String> fallback) {
Throwable e = resolveException(t);
if (e instanceof CancellationException) {

View File

@ -29,6 +29,7 @@ import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
@ -37,6 +38,7 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
public class MultiplayerServer extends Thread {
private ServerSocket socket;
private final String sessionName;
private final int gamePort;
private final boolean allowAllJoinRequests;
@ -49,7 +51,8 @@ public class MultiplayerServer extends Thread {
private final Map<String, Endpoint> clients = new ConcurrentHashMap<>();
private final Map<String, Endpoint> nameClientMap = new ConcurrentHashMap<>();
public MultiplayerServer(int gamePort, boolean allowAllJoinRequests) {
public MultiplayerServer(String sessionName, int gamePort, boolean allowAllJoinRequests) {
this.sessionName = sessionName;
this.gamePort = gamePort;
this.allowAllJoinRequests = allowAllJoinRequests;
@ -117,7 +120,7 @@ public class MultiplayerServer extends Thread {
try {
if (client.socket.isConnected()) {
client.write(new KickResponse(""));
client.write(new KickResponse(KickResponse.KICKED));
client.socket.close();
}
} catch (IOException e) {
@ -150,6 +153,17 @@ public class MultiplayerServer extends Thread {
LOG.info("Received join request with clientVersion=" + joinRequest.getClientVersion() + ", id=" + joinRequest.getUsername());
clientName = joinRequest.getUsername();
if (!Objects.equals(MultiplayerManager.CATO_VERSION, joinRequest.getClientVersion())) {
try {
endpoint.write(new KickResponse(KickResponse.VERSION_NOT_MATCHED));
LOG.info("Rejected join request from id=" + joinRequest.getUsername());
socket.close();
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to send kick response.", e);
return;
}
}
CatoClient catoClient = new CatoClient(this, clientName);
nameClientMap.put(clientName, endpoint);
onClientAdded.fireEvent(catoClient);
@ -157,7 +171,7 @@ public class MultiplayerServer extends Thread {
if (onClientAdding != null && !allowAllJoinRequests) {
onClientAdding.call(catoClient, () -> {
try {
endpoint.write(new JoinResponse(gamePort));
endpoint.write(new JoinResponse(sessionName, gamePort));
} catch (IOException e) {
LOG.log(Level.WARNING, "Failed to send join response.", e);
try {
@ -178,7 +192,7 @@ public class MultiplayerServer extends Thread {
});
} else {
// Allow all join requests.
endpoint.write(new JoinResponse(gamePort));
endpoint.write(new JoinResponse(sessionName, gamePort));
}
} else if (request instanceof KeepAliveRequest) {
endpoint.write(new KeepAliveResponse(System.currentTimeMillis()));

View File

@ -702,13 +702,14 @@ multiplayer.session.join.error.connection=Failed to join multiplayer session. Ca
multiplayer.session.join.hint=You must obtain the invitation code from the gamer who has already created a multiplayer session.
multiplayer.session.join.invitation_code=Invitation code
multiplayer.session.join.invitation_code.error=Incorrect invitation code. Please obtain invitation code from the player who creates the multiplayer session.
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=Lost connection: %s.
multiplayer.session.join.kicked.join_acceptance_timeout=The session holder does not accept your join request in time.
multiplayer.session.join.kicked.kicked=You have been kicked by the session holder.
multiplayer.session.join.kicked.version_not_matched=Versions of multiplayer functionalities are not the same among you.
multiplayer.session.join.lost_connection=Lost connection with the multiplayer session. Maybe the session is destroyed by the creator, or you cannot establish connection with this session.
multiplayer.session.join.port.error=Cannot find available local network port for listening. Please ensure that HMCL has the permission to listen on a port.
multiplayer.session.join.rejected=Your connection is rejected by the session holder.
multiplayer.session.join.wait=Waiting for acceptation.
multiplayer.session.join.wait_timeout=The session holder does not accept your join request in time.
multiplayer.session.members=Room Members
multiplayer.session.quit=Quit Room
multiplayer.session.quit.warning=After quiting room, you will lost the connection with the server. Continue?

View File

@ -701,13 +701,14 @@ multiplayer.session.join.error.connection=加入房間失敗。無法與對方
multiplayer.session.join.hint=你需要向已經創建好房間的玩家索要邀請碼以便加入多人聯機房間
multiplayer.session.join.invitation_code=邀請碼
multiplayer.session.join.invitation_code.error=邀請碼不正確,請向開服玩家獲取邀請碼
multiplayer.session.join.invitation_code.version=多人聯機功能版本號不一致,請保證連接多人聯機功能版本號一致。
multiplayer.session.join.kicked=你已經被房主踢出房間,你將與房間失去連接。
multiplayer.session.join.kicked=你與房間失去連接:%s。
multiplayer.session.join.kicked.join_acceptance_timeout=對方未能即時同意你的加入申請
multiplayer.session.join.kicked.kicked=你被房主踢出房間。
multiplayer.session.join.kicked.version_not_matched=多人聯機功能版本號不一致,請保證連接多人聯機功能版本號一致。
multiplayer.session.join.lost_connection=你已與房間失去連接。這可能意味著房主已經解散房間,或者你無法連接至房間。
multiplayer.session.join.port.error=無法找到可用的本地網路埠,請確保 HMCL 擁有綁定本地埠的權限。
multiplayer.session.join.rejected=你被房主拒絕連接。
multiplayer.session.join.wait=等待對方同意加入申請。
multiplayer.session.join.wait_timeout=對方未能即時同意你的加入申請
multiplayer.session.members=房間成員
multiplayer.session.quit=退出房間
multiplayer.session.quit.warning=退出房間後,你將會與伺服器斷開連接,是否繼續?

View File

@ -701,13 +701,14 @@ multiplayer.session.join.error.connection=加入房间失败。无法与对方
multiplayer.session.join.hint=你需要向已经创建好房间的玩家索要邀请码以便加入多人联机房间
multiplayer.session.join.invitation_code=邀请码
multiplayer.session.join.invitation_code.error=邀请码不正确,请向开服玩家获取邀请码
multiplayer.session.join.invitation_code.version=多人联机功能版本号不一致,请保证连接多人联机功能版本号一致。
multiplayer.session.join.kicked=你已经被房主踢出房间,你将与房间失去连接。
multiplayer.session.join.kicked=你与房间失去连接:%s。
multiplayer.session.join.kicked.join_acceptance_timeout=对方未能即时同意你的加入申请
multiplayer.session.join.kicked.kicked=你被房主踢出房间。
multiplayer.session.join.kicked.version_not_matched=多人联机功能版本号不一致,请保证连接多人联机功能版本号一致。
multiplayer.session.join.lost_connection=你已与房间失去连接。这可能意味着房主已经解散房间,或者你无法连接至房间。
multiplayer.session.join.port.error=无法找到可用的本地网络端口,请确保 HMCL 拥有绑定本地端口的权限。
multiplayer.session.join.rejected=你被房主拒绝连接。
multiplayer.session.join.wait=等待对方同意加入申请。
multiplayer.session.join.wait_timeout=对方未能即时同意你的加入申请
multiplayer.session.members=房间成员
multiplayer.session.quit=退出房间
multiplayer.session.quit.warning=退出房间后,你将会与服务器断开连接,是否继续?

View File

@ -31,7 +31,7 @@ public class MultiplayerClientServerTest {
public void startServer() throws Exception {
Logging.initForTest();
int localPort = MultiplayerManager.findAvailablePort();
MultiplayerServer server = new MultiplayerServer(1000, true);
MultiplayerServer server = new MultiplayerServer("SessionName", 1000, true);
server.startServer(localPort);
MultiplayerClient client = new MultiplayerClient("username", localPort);