diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java index f537b281d..526c36ec4 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/LocalServerBroadcaster.java @@ -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 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 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)); } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java index 855fcff2c..2e8544d4b 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerManager.java @@ -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 onExit = new EventManager<>(); private final EventManager onIPAllocated = new EventManager<>(); - private final EventManager onValidAt = new EventManager<>(); + private final EventManager 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 validAt = tryCast(logJson.get("valid"), String.class); - validAt.ifPresent(s -> onValidAt.fireEvent(new HiperShowValidAtEvent(this, s))); + Optional 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 onValidAt() { - return onValidAt; + public EventManager 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; } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java index 87b2b9fda..cdcafd8a6 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPage.java @@ -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 session = new ReadOnlyObjectWrapper<>(); private final IntegerProperty port = new SimpleIntegerProperty(); private final StringProperty address = new SimpleStringProperty(); - private final ReadOnlyObjectWrapper expireTime = new ReadOnlyObjectWrapper<>(); + private final ReadOnlyObjectWrapper expireTime = new ReadOnlyObjectWrapper<>(); private Consumer onExit; private Consumer onIPAllocated; - private Consumer onValidAt; + private Consumer onValidUntil; + + private final ReadOnlyObjectWrapper broadcaster = new ReadOnlyObjectWrapper<>(); + private Consumer 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 broadcasterProperty() { + return broadcaster; + } + + public void setBroadcaster(LocalServerBroadcaster broadcaster) { + this.broadcaster.set(broadcaster); + } + + public Date getExpireTime() { return expireTime.get(); } - public ReadOnlyObjectWrapper expireTimeProperty() { + public ReadOnlyObjectWrapper 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) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java index 1e03be647..19ef21624 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerPageSkin.java @@ -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 -> { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 18ab1220f..800d56267 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -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 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 024d32f00..7ba6ed492 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -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=資料包 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index 6b57d6a15..8635b5bcf 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -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=数据包