feat(multiplayer): kick player.
This commit is contained in:
@@ -70,7 +70,8 @@ public final class MultiplayerChannel {
|
||||
property = "type",
|
||||
subtypes = {
|
||||
@JsonSubtype(clazz = JoinResponse.class, name = "join"),
|
||||
@JsonSubtype(clazz = KeepAliveResponse.class, name = "keepalive")
|
||||
@JsonSubtype(clazz = KeepAliveResponse.class, name = "keepalive"),
|
||||
@JsonSubtype(clazz = KickResponse.class, name = "kick")
|
||||
}
|
||||
)
|
||||
public static class Response {
|
||||
@@ -101,6 +102,18 @@ public final class MultiplayerChannel {
|
||||
}
|
||||
}
|
||||
|
||||
public static class KickResponse extends Response {
|
||||
private final String msg;
|
||||
|
||||
public KickResponse(String msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CatoClient extends Event {
|
||||
private final String username;
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.io.*;
|
||||
import java.net.ConnectException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.*;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
@@ -38,6 +39,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<>();
|
||||
|
||||
public MultiplayerClient(String id, int port) {
|
||||
this.id = id;
|
||||
@@ -63,6 +65,10 @@ public class MultiplayerClient extends Thread {
|
||||
return onDisconnected;
|
||||
}
|
||||
|
||||
public EventManager<Event> onKicked() {
|
||||
return onDisconnected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
LOG.info("Connecting to 127.0.0.1:" + port);
|
||||
@@ -72,34 +78,37 @@ public class MultiplayerClient extends Thread {
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
|
||||
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);
|
||||
|
||||
String line = reader.readLine();
|
||||
if (line == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
JoinResponse response = JsonUtils.fromNonNullJson(line, JoinResponse.class);
|
||||
setGamePort(response.getPort());
|
||||
onConnected.fireEvent(new ConnectedEvent(this, response.getPort()));
|
||||
|
||||
LOG.fine("Received join response with port " + response.getPort());
|
||||
|
||||
while (!isInterrupted()) {
|
||||
writer.write(verifyJson(JsonUtils.UGLY_GSON.toJson(new KeepAliveResponse(System.currentTimeMillis()))));
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
|
||||
try {
|
||||
Thread.sleep(1500);
|
||||
} catch (InterruptedException ignored) {
|
||||
LOG.warning("MultiplayerClient interrupted");
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (isInterrupted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG.fine("Message from server:" + line);
|
||||
|
||||
Response response = JsonUtils.fromNonNullJson(line, Response.class);
|
||||
|
||||
if (response instanceof JoinResponse) {
|
||||
JoinResponse joinResponse = JsonUtils.fromNonNullJson(line, JoinResponse.class);
|
||||
setGamePort(joinResponse.getPort());
|
||||
onConnected.fireEvent(new ConnectedEvent(this, joinResponse.getPort()));
|
||||
|
||||
LOG.fine("Received join response with port " + joinResponse.getPort());
|
||||
} else if (response instanceof KickResponse) {
|
||||
onKicked.fireEvent(new Event(this));
|
||||
|
||||
LOG.fine("Kicked by the server");
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Unrecognized packet from server:" + line);
|
||||
}
|
||||
}
|
||||
} catch (ConnectException e) {
|
||||
LOG.info("Failed to connect to 127.0.0.1:" + port + ", tried " + i + " time(s)");
|
||||
|
||||
@@ -48,6 +48,7 @@ import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.logging.Level;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -146,6 +147,9 @@ public final class MultiplayerManager {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
client.onKicked().register(kickedEvent -> {
|
||||
future.completeExceptionally(new CancellationException());
|
||||
});
|
||||
client.start();
|
||||
});
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import javafx.collections.ObservableList;
|
||||
import javafx.scene.control.Control;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.event.Event;
|
||||
import org.jackhuang.hmcl.setting.DownloadProviders;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
@@ -41,11 +40,11 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.config;
|
||||
import static org.jackhuang.hmcl.setting.ConfigHolder.globalConfig;
|
||||
import static org.jackhuang.hmcl.ui.FXUtils.runInFX;
|
||||
import static org.jackhuang.hmcl.util.Logging.LOG;
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -211,6 +210,9 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
||||
int gamePort = result.getAd();
|
||||
try {
|
||||
MultiplayerManager.CatoSession session = MultiplayerManager.createSession(config().getMultiplayerToken(), result.getMotd(), gamePort);
|
||||
session.getServer().onClientAdding().register(event -> {
|
||||
|
||||
});
|
||||
session.getServer().onClientAdded().register(event -> {
|
||||
runInFX(() -> {
|
||||
clients.add(event);
|
||||
@@ -263,19 +265,37 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
||||
.thenAcceptAsync(session -> {
|
||||
initCatoSession(session);
|
||||
|
||||
AtomicBoolean kicked = new AtomicBoolean();
|
||||
|
||||
session.getClient().onDisconnected().register(() -> {
|
||||
runInFX(() -> {
|
||||
stopCatoSession();
|
||||
Controllers.dialog(i18n("multiplayer.session.join.lost_connection"));
|
||||
if (!kicked.get()) {
|
||||
Controllers.dialog(i18n("multiplayer.session.join.lost_connection"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
session.getClient().onKicked().register(() -> {
|
||||
runInFX(() -> {
|
||||
kicked.set(true);
|
||||
Controllers.dialog(i18n("multiplayer.session.join.kicked"));
|
||||
});
|
||||
});
|
||||
|
||||
gamePort.set(session.getClient().getGamePort());
|
||||
setMultiplayerState(MultiplayerManager.State.SLAVE);
|
||||
resolve.run();
|
||||
}, Platform::runLater).exceptionally(throwable -> {
|
||||
LOG.log(Level.WARNING, "Failed to join sessoin");
|
||||
reject.accept(i18n("multiplayer.session.error"));
|
||||
}, Platform::runLater)
|
||||
.exceptionally(throwable -> {
|
||||
if (throwable instanceof CancellationException) {
|
||||
LOG.info("Connection rejected by the server");
|
||||
reject.accept(i18n("multiplayer.session.join.rejected"));
|
||||
return null;
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Failed to join sessoin");
|
||||
reject.accept(i18n("multiplayer.session.error"));
|
||||
}
|
||||
return null;
|
||||
});
|
||||
} catch (MultiplayerManager.IncompatibleCatoVersionException e) {
|
||||
@@ -286,6 +306,17 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware
|
||||
.addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.join.invitation_code"), "", new RequiredValidator())));
|
||||
}
|
||||
|
||||
public void kickPlayer(MultiplayerChannel.CatoClient client) {
|
||||
if (getSession() == null || !getSession().isReady() || getMultiplayerState() != MultiplayerManager.State.MASTER) {
|
||||
throw new IllegalStateException("CatoSession not ready");
|
||||
}
|
||||
|
||||
Controllers.confirm(i18n("multiplayer.session.create.members.kick.prompt"), i18n("multiplayer.session.create.members.kick"), MessageDialogPane.MessageType.WARNING,
|
||||
() -> {
|
||||
getSession().getServer().kickPlayer(client);
|
||||
}, null);
|
||||
}
|
||||
|
||||
public void closeRoom() {
|
||||
if (getSession() == null || !getSession().isReady() || getMultiplayerState() != MultiplayerManager.State.MASTER) {
|
||||
throw new IllegalStateException("CatoSession not ready");
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.jackhuang.hmcl.Metadata;
|
||||
import org.jackhuang.hmcl.game.LauncherHelper;
|
||||
import org.jackhuang.hmcl.setting.Profile;
|
||||
import org.jackhuang.hmcl.setting.Profiles;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
@@ -315,12 +316,18 @@ public class MultiplayerPageSkin extends SkinBase<MultiplayerPage> {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ClientItem extends StackPane {
|
||||
private class ClientItem extends StackPane {
|
||||
ClientItem(MultiplayerChannel.CatoClient client) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.setPadding(new Insets(8));
|
||||
pane.setLeft(new Label(client.getUsername()));
|
||||
|
||||
JFXButton kickButton = new JFXButton();
|
||||
kickButton.setGraphic(SVG.close(Theme.blackFillBinding(), 16, 16));
|
||||
kickButton.getStyleClass().add("toggle-icon-tiny");
|
||||
kickButton.setOnAction(e -> getSkinnable().kickPlayer(client));
|
||||
pane.setRight(kickButton);
|
||||
|
||||
RipplerContainer container = new RipplerContainer(pane);
|
||||
getChildren().setAll(container);
|
||||
getStyleClass().add("md-list-cell");
|
||||
|
||||
@@ -20,12 +20,17 @@ 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;
|
||||
import org.jackhuang.hmcl.util.gson.JsonUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.ServerSocket;
|
||||
import java.net.Socket;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.logging.Level;
|
||||
|
||||
import static org.jackhuang.hmcl.ui.multiplayer.MultiplayerChannel.*;
|
||||
@@ -35,9 +40,12 @@ public class MultiplayerServer extends Thread {
|
||||
private ServerSocket socket;
|
||||
private final int gamePort;
|
||||
|
||||
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<>();
|
||||
|
||||
public MultiplayerServer(int gamePort) {
|
||||
this.gamePort = gamePort;
|
||||
@@ -46,6 +54,10 @@ public class MultiplayerServer extends Thread {
|
||||
setDaemon(true);
|
||||
}
|
||||
|
||||
public void setOnClientAdding(FutureCallback<CatoClient> callback) {
|
||||
onClientAdding = callback;
|
||||
}
|
||||
|
||||
public EventManager<MultiplayerChannel.CatoClient> onClientAdded() {
|
||||
return onClientAdded;
|
||||
}
|
||||
@@ -54,10 +66,6 @@ public class MultiplayerServer extends Thread {
|
||||
return onClientDisconnected;
|
||||
}
|
||||
|
||||
public EventManager<Event> onKeepAlive() {
|
||||
return onKeepAlive;
|
||||
}
|
||||
|
||||
public void startServer() throws IOException {
|
||||
startServer(0);
|
||||
}
|
||||
@@ -92,12 +100,31 @@ public class MultiplayerServer extends Thread {
|
||||
}
|
||||
}
|
||||
|
||||
public void kickPlayer(CatoClient player) {
|
||||
Client client = nameClientMap.get(player.getUsername());
|
||||
if (client == null) return;
|
||||
|
||||
try {
|
||||
if (client.socket.isConnected()) {
|
||||
client.write(new KickResponse());
|
||||
client.socket.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to kick player " + player.getUsername() + ". Maybe already disconnected?", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleClient(Socket targetSocket) {
|
||||
String address = targetSocket.getRemoteSocketAddress().toString();
|
||||
String clientName = null;
|
||||
LOG.info("Accepted client " + targetSocket.getRemoteSocketAddress());
|
||||
LOG.info("Accepted client " + address);
|
||||
try (Socket clientSocket = targetSocket;
|
||||
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);
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (isInterrupted()) {
|
||||
@@ -110,19 +137,35 @@ public class MultiplayerServer extends Thread {
|
||||
if (request instanceof JoinRequest) {
|
||||
JoinRequest joinRequest = (JoinRequest) request;
|
||||
LOG.info("Received join request with clientVersion=" + joinRequest.getClientVersion() + ", id=" + joinRequest.getUsername());
|
||||
|
||||
writer.write(verifyJson(JsonUtils.UGLY_GSON.toJson(new JoinResponse(gamePort))));
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
|
||||
clientName = joinRequest.getUsername();
|
||||
onClientAdded.fireEvent(new CatoClient(this, joinRequest.getUsername()));
|
||||
} else if (request instanceof KeepAliveRequest) {
|
||||
writer.write(JsonUtils.UGLY_GSON.toJson(new KeepAliveResponse(System.currentTimeMillis())));
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
|
||||
onKeepAlive.fireEvent(new Event(this));
|
||||
CatoClient catoClient = new CatoClient(this, clientName);
|
||||
nameClientMap.put(clientName, client);
|
||||
onClientAdded.fireEvent(catoClient);
|
||||
|
||||
if (onClientAdding != null) {
|
||||
onClientAdding.call(catoClient, () -> {
|
||||
try {
|
||||
client.write(new JoinResponse(gamePort));
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to send join response.", e);
|
||||
try {
|
||||
socket.close();
|
||||
} catch (IOException ioException) {
|
||||
LOG.log(Level.WARNING, "Failed to close socket caused by join response sending failure.", e);
|
||||
this.interrupt();
|
||||
}
|
||||
}
|
||||
}, msg -> {
|
||||
try {
|
||||
client.write(new KickResponse(msg));
|
||||
LOG.info("Rejected join request from id=" + joinRequest.getUsername());
|
||||
socket.close();
|
||||
} catch (IOException e) {
|
||||
LOG.log(Level.WARNING, "Failed to send kick response.", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Unrecognized packet from client " + targetSocket.getRemoteSocketAddress() + ":" + line);
|
||||
}
|
||||
@@ -135,6 +178,25 @@ public class MultiplayerServer extends Thread {
|
||||
if (clientName != null) {
|
||||
onClientDisconnected.fireEvent(new CatoClient(this, clientName));
|
||||
}
|
||||
|
||||
clients.remove(address);
|
||||
if (clientName != null) nameClientMap.remove(clientName);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Client {
|
||||
public final Socket socket;
|
||||
public final BufferedWriter writer;
|
||||
|
||||
public Client(Socket socket, BufferedWriter writer) {
|
||||
this.socket = socket;
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
public synchronized void write(Object object) throws IOException {
|
||||
writer.write(verifyJson(JsonUtils.UGLY_GSON.toJson(object)));
|
||||
writer.newLine();
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,8 +644,10 @@ multiplayer.session.join.hint=You must obtain the invitation code from the gamer
|
||||
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.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.
|
||||
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.members=Room Members
|
||||
multiplayer.session.quit=Quit Room
|
||||
multiplayer.session.quit.warning=After quiting room, you will lost the connection with the server. Continue?
|
||||
|
||||
@@ -643,8 +643,10 @@ 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.lost_connection=你已與房間失去連接。這可能意味著房主已經解散房間,或者你無法連接至房間。
|
||||
multiplayer.session.join.port.error=無法找到可用的本地網路埠,請確保 HMCL 擁有綁定本地埠的權限。
|
||||
multiplayer.session.join.rejected=你被房主拒絕連接。
|
||||
multiplayer.session.members=房間成員
|
||||
multiplayer.session.quit=退出房間
|
||||
multiplayer.session.quit.warning=退出房間後,你將會與伺服器斷開連接,是否繼續?
|
||||
|
||||
@@ -643,8 +643,10 @@ 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.lost_connection=你已与房间失去连接。这可能意味着房主已经解散房间,或者你无法连接至房间。
|
||||
multiplayer.session.join.port.error=无法找到可用的本地网络端口,请确保 HMCL 拥有绑定本地端口的权限。
|
||||
multiplayer.session.join.rejected=你被房主拒绝连接。
|
||||
multiplayer.session.members=房间成员
|
||||
multiplayer.session.quit=退出房间
|
||||
multiplayer.session.quit.warning=退出房间后,你将会与服务器断开连接,是否继续?
|
||||
|
||||
Reference in New Issue
Block a user