feat(multiplayer): broadcast address to local network.

This commit is contained in:
huanghongxun 2022-09-24 18:31:14 +08:00
parent 4d54c7f6ba
commit e5ea4cd79f
7 changed files with 147 additions and 28 deletions

View File

@ -17,6 +17,8 @@
*/
package org.jackhuang.hmcl.ui.multiplayer;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.event.EventManager;
import org.jackhuang.hmcl.util.Lang;
import java.io.IOException;
@ -36,6 +38,8 @@ public class LocalServerBroadcaster implements AutoCloseable {
private final String address;
private final ThreadGroup threadGroup = new ThreadGroup("JoinSession");
private final EventManager<Event> onExit = new EventManager<>();
private boolean running = true;
public LocalServerBroadcaster(String address) {
@ -49,6 +53,14 @@ public class LocalServerBroadcaster implements AutoCloseable {
threadGroup.interrupt();
}
public String getAddress() {
return address;
}
public EventManager<Event> onExit() {
return onExit;
}
public static final Pattern ADDRESS_PATTERN = Pattern.compile("^\\s*(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}):(\\d{1,5})\\s*$");
public void start() {
@ -77,7 +89,9 @@ public class LocalServerBroadcaster implements AutoCloseable {
}
} catch (IOException e) {
LOG.log(Level.WARNING, "Error in forwarding port", e);
threadGroup.interrupt();
} finally {
close();
onExit.fireEvent(new Event(this));
}
}

View File

@ -25,6 +25,7 @@ import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.gson.DateTypeAdapter;
import org.jackhuang.hmcl.util.gson.JsonUtils;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import org.jackhuang.hmcl.util.io.FileUtils;
@ -218,7 +219,7 @@ public final class MultiplayerManager {
public static class HiperSession extends ManagedProcess {
private final EventManager<HiperExitEvent> onExit = new EventManager<>();
private final EventManager<HiperIPEvent> onIPAllocated = new EventManager<>();
private final EventManager<HiperShowValidAtEvent> onValidAt = new EventManager<>();
private final EventManager<HiperShowValidUntilEvent> onValidUntil = new EventManager<>();
private final BufferedWriter writer;
private int error = 0;
@ -256,8 +257,15 @@ public final class MultiplayerManager {
error = HiperExitEvent.FAILED_LOAD_CONFIG;
}
if (msg.contains("Validity of client certificate")) {
Optional<String> validAt = tryCast(logJson.get("valid"), String.class);
validAt.ifPresent(s -> onValidAt.fireEvent(new HiperShowValidAtEvent(this, s)));
Optional<String> validUntil = tryCast(logJson.get("valid"), String.class);
if (validUntil.isPresent()) {
try {
Date date = DateTypeAdapter.deserializeToDate(validUntil.get());
onValidUntil.fireEvent(new HiperShowValidUntilEvent(this, date));
} catch (JsonParseException e) {
LOG.log(Level.WARNING, "Failed to parse certification expire time string: " + validUntil.get());
}
}
}
}
@ -303,8 +311,8 @@ public final class MultiplayerManager {
return onIPAllocated;
}
public EventManager<HiperShowValidAtEvent> onValidAt() {
return onValidAt;
public EventManager<HiperShowValidUntilEvent> onValidUntil() {
return onValidUntil;
}
}
@ -341,15 +349,15 @@ public final class MultiplayerManager {
}
}
public static class HiperShowValidAtEvent extends Event {
private final String validAt;
public static class HiperShowValidUntilEvent extends Event {
private final Date validAt;
public HiperShowValidAtEvent(Object source, String validAt) {
public HiperShowValidUntilEvent(Object source, Date validAt) {
super(source);
this.validAt = validAt;
}
public String getValidAt() {
public Date getValidUntil() {
return validAt;
}
}

View File

@ -22,6 +22,7 @@ import com.jfoenix.controls.JFXDialogLayout;
import javafx.beans.property.*;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import org.jackhuang.hmcl.event.Event;
import org.jackhuang.hmcl.setting.DownloadProviders;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.ui.Controllers;
@ -33,6 +34,7 @@ import org.jackhuang.hmcl.util.TaskCancellationAction;
import org.jackhuang.hmcl.util.io.ChecksumMismatchException;
import java.time.Instant;
import java.util.Date;
import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import java.util.logging.Level;
@ -49,11 +51,14 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
private final ReadOnlyObjectWrapper<MultiplayerManager.HiperSession> session = new ReadOnlyObjectWrapper<>();
private final IntegerProperty port = new SimpleIntegerProperty();
private final StringProperty address = new SimpleStringProperty();
private final ReadOnlyObjectWrapper<Instant> expireTime = new ReadOnlyObjectWrapper<>();
private final ReadOnlyObjectWrapper<Date> expireTime = new ReadOnlyObjectWrapper<>();
private Consumer<MultiplayerManager.HiperExitEvent> onExit;
private Consumer<MultiplayerManager.HiperIPEvent> onIPAllocated;
private Consumer<MultiplayerManager.HiperShowValidAtEvent> onValidAt;
private Consumer<MultiplayerManager.HiperShowValidUntilEvent> onValidUntil;
private final ReadOnlyObjectWrapper<LocalServerBroadcaster> broadcaster = new ReadOnlyObjectWrapper<>();
private Consumer<Event> onBroadcasterExit = null;
public MultiplayerPage() {
}
@ -92,15 +97,27 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
this.address.set(address);
}
public Instant getExpireTime() {
public LocalServerBroadcaster getBroadcaster() {
return broadcaster.get();
}
public ReadOnlyObjectWrapper<LocalServerBroadcaster> broadcasterProperty() {
return broadcaster;
}
public void setBroadcaster(LocalServerBroadcaster broadcaster) {
this.broadcaster.set(broadcaster);
}
public Date getExpireTime() {
return expireTime.get();
}
public ReadOnlyObjectWrapper<Instant> expireTimeProperty() {
public ReadOnlyObjectWrapper<Date> expireTimeProperty() {
return expireTime;
}
public void setExpireTime(Instant expireTime) {
public void setExpireTime(Date expireTime) {
this.expireTime.set(expireTime);
}
@ -195,7 +212,7 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
this.session.set(session);
onExit = session.onExit().registerWeak(this::onExit);
onIPAllocated = session.onIPAllocated().registerWeak(this::onIPAllocated);
onValidAt = session.onValidAt().registerWeak(this::onValidAt);
onValidUntil = session.onValidUntil().registerWeak(this::onValidUntil);
}, Schedulers.javafx())
.exceptionally(throwable -> {
runInFX(() -> Controllers.dialog(localizeErrorMessage(throwable), null, MessageDialogPane.MessageType.ERROR));
@ -207,22 +224,44 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
if (getSession() != null) {
getSession().stop();
}
if (getBroadcaster() != null) {
getBroadcaster().close();
}
clearSession();
}
public void broadcast(String url) {
LocalServerBroadcaster broadcaster = new LocalServerBroadcaster(url);
this.onBroadcasterExit = broadcaster.onExit().registerWeak(this::onBroadcasterExit);
broadcaster.start();
this.broadcaster.set(broadcaster);
}
public void stopBroadcasting() {
if (getBroadcaster() != null) {
getBroadcaster().close();
}
}
private void onBroadcasterExit(Event event) {
this.broadcaster.set(null);
}
private void clearSession() {
this.session.set(null);
this.onExit = null;
this.onIPAllocated = null;
this.onValidAt = null;
this.onValidUntil = null;
this.broadcaster.set(null);
this.onBroadcasterExit = null;
}
private void onIPAllocated(MultiplayerManager.HiperIPEvent event) {
runInFX(() -> this.address.set(event.getIP()));
}
private void onValidAt(MultiplayerManager.HiperShowValidAtEvent event) {
runInFX(() -> this.expireTime.set(event.getValidAt()));
private void onValidUntil(MultiplayerManager.HiperShowValidUntilEvent event) {
runInFX(() -> this.expireTime.set(event.getValidUntil()));
}
private void onExit(MultiplayerManager.HiperExitEvent event) {

View File

@ -40,6 +40,7 @@ import org.jackhuang.hmcl.ui.decorator.DecoratorAnimatedPage;
import org.jackhuang.hmcl.ui.versions.Versions;
import org.jackhuang.hmcl.util.HMCLService;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.i18n.Locales;
import java.time.format.DateTimeFormatter;
import java.time.format.FormatStyle;
@ -135,12 +136,11 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
ComponentList onPane = new ComponentList();
{
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM);
BorderPane expirationPane = new BorderPane();
expirationPane.setLeft(new Label(i18n("multiplayer.session.expiration")));
Label expirationLabel = new Label();
expirationLabel.textProperty().bind(Bindings.createStringBinding(() ->
control.getExpireTime() == null ? "" : formatter.format(control.getExpireTime()),
control.getExpireTime() == null ? "" : Locales.SIMPLE_DATE_FORMAT.get().format(control.getExpireTime()),
control.expireTimeProperty()));
expirationPane.setCenter(expirationLabel);
@ -158,10 +158,10 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
GridPane.setColumnSpan(title, 3);
masterPane.addRow(0, title);
HintPane masterHintPane = new HintPane(MessageDialogPane.MessageType.INFO);
GridPane.setColumnSpan(masterHintPane, 3);
masterHintPane.setText(i18n("multiplayer.master.hint"));
masterPane.addRow(1, masterHintPane);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
GridPane.setColumnSpan(hintPane, 3);
hintPane.setText(i18n("multiplayer.master.hint"));
masterPane.addRow(1, hintPane);
Label portTitle = new Label(i18n("multiplayer.master.port"));
BorderPane.setAlignment(portTitle, Pos.CENTER_LEFT);
@ -200,9 +200,58 @@ public class MultiplayerPageSkin extends DecoratorAnimatedPage.DecoratorAnimated
VBox slavePane = new VBox(8);
{
HintPane slaveHintPane = new HintPane(MessageDialogPane.MessageType.INFO);
slaveHintPane.setText(i18n("multiplayer.slave.hint"));
slavePane.getChildren().setAll(new Label(i18n("multiplayer.slave")), slaveHintPane);
Label title = new Label(i18n("multiplayer.slave"));
GridPane.setColumnSpan(title, 3);
slavePane.getChildren().add(title);
HintPane hintPane = new HintPane(MessageDialogPane.MessageType.INFO);
GridPane.setColumnSpan(hintPane, 3);
hintPane.setText(i18n("multiplayer.slave.hint"));
slavePane.getChildren().add(hintPane);
GridPane notBroadcastingPane = new GridPane();
{
notBroadcastingPane.setVgap(8);
notBroadcastingPane.setHgap(16);
notBroadcastingPane.getColumnConstraints().setAll(titleColumn, valueColumn, rightColumn);
Label addressTitle = new Label(i18n("multiplayer.slave.server_address"));
JFXTextField addressField = new JFXTextField();
GridPane.setColumnSpan(addressField, 2);
FXUtils.setValidateWhileTextChanged(addressField, true);
addressField.getValidators().add(new URLValidator());
JFXButton startButton = new JFXButton(i18n("multiplayer.master.server_address.start"));
startButton.setOnAction(e -> control.broadcast(addressField.getText()));
notBroadcastingPane.addRow(2, addressTitle, addressField, startButton);
}
GridPane broadcastingPane = new GridPane();
{
notBroadcastingPane.setVgap(8);
notBroadcastingPane.setHgap(16);
notBroadcastingPane.getColumnConstraints().setAll(titleColumn, valueColumn, rightColumn);
Label addressTitle = new Label(i18n("multiplayer.slave.server_address"));
Label addressLabel = new Label();
addressLabel.textProperty().bind(Bindings.createStringBinding(() ->
control.getBroadcaster() != null ? control.getBroadcaster().getAddress() : "",
control.broadcasterProperty()));
GridPane.setColumnSpan(addressLabel, 2);
JFXButton stopButton = new JFXButton(i18n("multiplayer.slave.server_address.stop"));
stopButton.setOnAction(e -> control.stopBroadcasting());
notBroadcastingPane.addRow(2, addressTitle, addressLabel, stopButton);
}
FXUtils.onChangeAndOperate(control.broadcasterProperty(), broadcaster -> {
if (broadcaster == null) {
slavePane.getChildren().setAll(title, hintPane, notBroadcastingPane);
} else {
slavePane.getChildren().setAll(title, hintPane, broadcastingPane);
}
});
}
FXUtils.onChangeAndOperate(control.expireTimeProperty(), t -> {

View File

@ -872,6 +872,9 @@ multiplayer.master.port=Port number
multiplayer.master.port.validate=The port number (0~65535) displayed in the game chat box,when you open the game in LAN.
multiplayer.slave=Participant Prompt
multiplayer.slave.hint=If you want to join another player's game save to play the game, you need to ask that player to turn on the open mode to the local area network according to the operation prompted by the creator. Then start the game, and select the multiplayer mode, select Add Server. The game will ask you to enter the server address, you only need to create a player to ask for the server address and enter it, and then enter the server.
multiplayer.slave.server_address=Creator server address
multiplayer.slave.server_address.start=Join
multiplayer.slave.server_address.stop=Exit
multiplayer.session.expiration=Expire Time
datapack=Datapacks

View File

@ -720,6 +720,9 @@ multiplayer.master.port.validate=在遊戲聊天框中出現的埠號 (0~65535)
multiplayer.slave=參與者提示
multiplayer.slave.hint=1.要求創建方按照上方的 創建方提示 操作\n2.啟動遊戲\n3.選擇多人遊戲模式,選擇添加伺服器\n4.遊戲會要求你輸入伺服器地址,你只需要向創建方索要伺服器地址並輸入,並進入伺服器即可。 \n- 注意:\n1.一般情況下,參與者的遊戲賬戶必須是 微軟賬戶 或 外置登錄賬戶(如 Little Skin否則加入失敗具體操作方法詳見左側的 幫助 \n2.一般情況下,參與者的遊戲版本、模組要必須與創建方的一致,否則加入失敗。
#(若加回 LocalServerBroadcaster.java就使用)multiplayer.slave.hint=1.要求創建方按照上方的 創建方提示 操作\n2.啟動遊戲\n3.選擇多人遊戲模式,選擇添加伺服器\n4.遊戲會要求你輸入伺服器地址,你只需要向創建方索要伺服器地址並輸入,並進入伺服器即可。 \n- 注意:\n1.一般情況下,參與者的遊戲賬戶必須是 微軟賬戶 或 外置登錄賬戶(如 Little Skin否則你需要將伺服器地址輸入至下方的輸入框中並點擊廣播在遊戲中選擇多人遊戲模式進入局域網世界方可加入具體操作方法詳見左側的 幫助 \n2.一般情況下,參與者的遊戲版本、模組要必須與創建方的一致,否則加入失敗。
multiplayer.slave.server_address=創建方服務器地址
multiplayer.slave.server_address.start=加入
multiplayer.slave.server_address.stop=退出
multiplayer.session.expiration=本次使用截止時間
datapack=資料包

View File

@ -720,6 +720,9 @@ multiplayer.master.port.validate=在游戏聊天框中出现的端口号 (0~6553
multiplayer.slave=参与者提示
multiplayer.slave.hint=1.要求创建方按照上方的 创建方提示 操作\n2.启动游戏\n3.选择多人游戏模式,选择添加服务器\n4.游戏会要求你输入服务器地址,你只需要向创建方索要服务器地址并输入,并进入服务器即可。\n- 注意:\n1.一般情况下,参与者的游戏账户必须是 微软账户 或 外置登录账户(如 Little Skin否则加入失败具体操作方法详见左侧的 帮助 \n2.一般情况下,参与者的游戏版本、模组要必须与创建方的一致,否则加入失败。
#(若加回 LocalServerBroadcaster.java就使用)multiplayer.slave.hint=1.要求创建方按照上方的 创建方提示 操作\n2.启动游戏\n3.选择多人游戏模式,选择添加服务器\n4.游戏会要求你输入服务器地址,你只需要向创建方索要服务器地址并输入,并进入服务器即可。\n- 注意:\n1.一般情况下,参与者的游戏账户必须是 微软账户 或 外置登录账户(如 Little Skin否则你需要将服务器地址输入至下方的输入框中并点击广播在游戏中选择多人游戏模式进入局域网世界方可加入具体操作方法详见左侧的 帮助 \n2.一般情况下,参与者的游戏版本、模组要必须与创建方的一致,否则加入失败。
multiplayer.slave.server_address=创建方服务器地址
multiplayer.slave.server_address.start=加入
multiplayer.slave.server_address.stop=退出
multiplayer.session.expiration=本次使用截止时间
datapack=数据包