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 45b304dae..f3a76d2c5 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 @@ -51,7 +51,7 @@ import static org.jackhuang.hmcl.util.Logging.LOG; */ public final class MultiplayerManager { private static final String CATO_DOWNLOAD_URL = "https://files.huangyuhui.net/maven/"; - private static final String CATO_VERSION = "2021-09-18"; + private static final String CATO_VERSION = "2021-09-20"; private static final Artifact CATO_ARTIFACT = new Artifact("cato", "cato", CATO_VERSION, OperatingSystem.CURRENT_OS.getCheckedName() + "-" + Architecture.CURRENT.name().toLowerCase(Locale.ROOT), OperatingSystem.CURRENT_OS == OperatingSystem.WINDOWS ? "exe" : null); @@ -86,7 +86,7 @@ public final class MultiplayerManager { .command(commands) .start(); - CatoSession session = new CatoSession(sessionName, process, Arrays.asList(commands)); + CatoSession session = new CatoSession(sessionName, State.SLAVE, process, Arrays.asList(commands)); session.addRelatedThread(Lang.thread(new LocalServerBroadcaster(localPort, session), "LocalServerBroadcaster", true)); return session; } @@ -96,12 +96,12 @@ public final class MultiplayerManager { if (!Files.isRegularFile(exe)) { throw new IllegalStateException("Cato file not found"); } - String[] commands = new String[]{exe.toString(), "--token", "new", "--allow", String.format("127.0.0.1:%d", port)}; + String[] commands = new String[]{exe.toString(), "--token", "new", "--allows", String.format("127.0.0.1:%d", port)}; Process process = new ProcessBuilder() .command(commands) .start(); - return new CatoSession(sessionName, process, Arrays.asList(commands)); + return new CatoSession(sessionName, State.MASTER, process, Arrays.asList(commands)); } public static Invitation parseInvitationCode(String invitationCode) throws JsonParseException { @@ -118,35 +118,46 @@ public final class MultiplayerManager { public static class CatoSession extends ManagedProcess { private final EventManager onExit = new EventManager<>(); private final EventManager onIdGenerated = new EventManager<>(); + private final EventManager onPeerConnected = new EventManager<>(); private final String name; + private final State type; private String id; - CatoSession(String name, Process process, List commands) { + CatoSession(String name, State type, Process process, List commands) { super(process, commands); LOG.info("Started cato with command: " + new CommandBuilder().addAll(commands).toString()); this.name = name; + this.type = type; addRelatedThread(Lang.thread(this::waitFor, "CatoExitWaiter", true)); addRelatedThread(Lang.thread(new StreamPump(process.getInputStream(), this::checkCatoLog), "CatoInputStreamPump", true)); addRelatedThread(Lang.thread(new StreamPump(process.getErrorStream(), this::checkCatoLog), "CatoErrorStreamPump", true)); } private void checkCatoLog(String log) { + LOG.info("Cato: " + log); if (id == null) { - LOG.info("Cato: " + log); Matcher matcher = TEMP_TOKEN_PATTERN.matcher(log); if (matcher.find()) { id = "mix" + matcher.group("id"); onIdGenerated.fireEvent(new CatoIdEvent(this, id)); } } + + { + Matcher matcher = PEER_CONNECTED_PATTERN.matcher(log); + if (matcher.find()) { + onPeerConnected.fireEvent(new Event(this)); + } + } } private void waitFor() { try { int exitCode = getProcess().waitFor(); + LOG.info("cato exited with exitcode " + exitCode); onExit.fireEvent(new CatoExitEvent(this, exitCode)); } catch (InterruptedException e) { onExit.fireEvent(new CatoExitEvent(this, CatoExitEvent.EXIT_CODE_INTERRUPTED)); @@ -161,6 +172,10 @@ public final class MultiplayerManager { return name; } + public State getType() { + return type; + } + @Nullable public String getId() { return id; @@ -178,7 +193,16 @@ public final class MultiplayerManager { return onExit; } + public EventManager onIdGenerated() { + return onIdGenerated; + } + + public EventManager onPeerConnected() { + return onPeerConnected; + } + private static final Pattern TEMP_TOKEN_PATTERN = Pattern.compile("id\\(mix(?\\w+)\\)"); + private static final Pattern PEER_CONNECTED_PATTERN = Pattern.compile("Peer connected"); } public static class CatoExitEvent extends Event { @@ -212,6 +236,7 @@ public final class MultiplayerManager { enum State { DISCONNECTED, + CONNECTING, MASTER, SLAVE } 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 ce3f3debd..d6b09f2b5 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 de.javawi.jstun.test.DiscoveryTest; import javafx.beans.property.*; import javafx.scene.control.Control; 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.task.Task; @@ -46,11 +47,14 @@ public class MultiplayerPage extends Control implements DecoratorPage { private final ReadOnlyObjectWrapper state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("multiplayer"), -1)); private final ObjectProperty multiplayerState = new SimpleObjectProperty<>(MultiplayerManager.State.DISCONNECTED); + private final ReadOnlyStringWrapper token = new ReadOnlyStringWrapper(); private final ReadOnlyObjectWrapper natState = new ReadOnlyObjectWrapper<>(); private final ReadOnlyIntegerWrapper port = new ReadOnlyIntegerWrapper(-1); private final ReadOnlyObjectWrapper session = new ReadOnlyObjectWrapper<>(); private Consumer onExit; + private Consumer onIdGenerated; + private Consumer onPeerConnected; public MultiplayerPage() { testNAT(); @@ -82,6 +86,14 @@ public class MultiplayerPage extends Control implements DecoratorPage { return natState.getReadOnlyProperty(); } + public String getToken() { + return token.get(); + } + + public ReadOnlyStringProperty tokenProperty() { + return token.getReadOnlyProperty(); + } + public int getPort() { return port.get(); } @@ -158,7 +170,7 @@ public class MultiplayerPage extends Control implements DecoratorPage { } this.port.set(port); - setMultiplayerState(MultiplayerManager.State.MASTER); + setMultiplayerState(MultiplayerManager.State.CONNECTING); resolve.run(); }) .addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("multiplayer.session.create.hint"))) @@ -171,7 +183,7 @@ public class MultiplayerPage extends Control implements DecoratorPage { throw new IllegalStateException("CatoSession already ready"); } - Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.join.prompt"), (result, resolve, reject) -> { + Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.join"), (result, resolve, reject) -> { String invitationCode = ((PromptDialogPane.Builder.StringQuestion) result.get(1)).getValue(); MultiplayerManager.Invitation invitation; try { @@ -198,10 +210,10 @@ public class MultiplayerPage extends Control implements DecoratorPage { } port.set(localPort); - setMultiplayerState(MultiplayerManager.State.SLAVE); + setMultiplayerState(MultiplayerManager.State.CONNECTING); resolve.run(); }) - .addQuestion(new PromptDialogPane.Builder.HintQuestion("multiplayer.session.join.hint")) + .addQuestion(new PromptDialogPane.Builder.HintQuestion(i18n("multiplayer.session.join.hint"))) .addQuestion(new PromptDialogPane.Builder.StringQuestion(i18n("multiplayer.session.join.invitation_code"), "", new RequiredValidator()))); } @@ -213,8 +225,7 @@ public class MultiplayerPage extends Control implements DecoratorPage { Controllers.confirm(i18n("multiplayer.session.close.warning"), i18n("message.warning"), MessageDialogPane.MessageType.WARNING, () -> { getSession().stop(); - session.set(null); - setMultiplayerState(MultiplayerManager.State.DISCONNECTED); + clearCatoSession(); }, null); } @@ -224,22 +235,51 @@ public class MultiplayerPage extends Control implements DecoratorPage { } getSession().stop(); - session.set(null); - setMultiplayerState(MultiplayerManager.State.DISCONNECTED); + clearCatoSession(); } private void initCatoSession(MultiplayerManager.CatoSession session) { runInFX(() -> { - session.onExit().registerWeak(this::onCatoExit); + onExit = session.onExit().registerWeak(this::onCatoExit); + onIdGenerated = session.onIdGenerated().registerWeak(this::onCatoIdGenerated); + onPeerConnected = session.onPeerConnected().registerWeak(this::onCatoPeerConnected); this.session.set(session); }); } + private void clearCatoSession() { + this.session.set(null); + this.token.set(null); + this.port.set(-1); + this.multiplayerState.set(MultiplayerManager.State.DISCONNECTED); + } + private void onCatoExit(MultiplayerManager.CatoExitEvent event) { - if (event.getExitCode() == MultiplayerManager.CatoExitEvent.EXIT_CODE_SESSION_EXPIRED) { - Controllers.dialog(i18n("multiplayer.session.expired")); - } + runInFX(() -> { + if (event.getExitCode() == MultiplayerManager.CatoExitEvent.EXIT_CODE_SESSION_EXPIRED) { + Controllers.dialog(i18n("multiplayer.session.expired")); + } else if (event.getExitCode() != 0) { + if (!((MultiplayerManager.CatoSession) event.getSource()).isReady()) { + Controllers.dialog(i18n("multiplayer.exit.before_ready", event.getExitCode())); + } else { + Controllers.dialog(i18n("multiplayer.exit.after_ready", event.getExitCode())); + } + } + clearCatoSession(); + }); + } + + private void onCatoPeerConnected(Event event) { + runInFX(() -> { + }); + } + + private void onCatoIdGenerated(MultiplayerManager.CatoIdEvent event) { + runInFX(() -> { + token.set(event.getId()); + multiplayerState.set(((MultiplayerManager.CatoSession) event.getSource()).getType()); + }); } @Override 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 f702f6c62..a2d9d5ce1 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 @@ -125,6 +125,13 @@ public class MultiplayerPageSkin extends SkinBase { disconnectedPane.getChildren().setAll(hintPane, label); } + VBox connectingPane = new VBox(8); + { + Label label = new Label(i18n("multiplayer.state.connecting")); + + connectingPane.getChildren().setAll(label); + } + VBox masterPane = new VBox(8); { Label label = new Label(i18n("multiplayer.state.master")); @@ -149,6 +156,8 @@ public class MultiplayerPageSkin extends SkinBase { FXUtils.onChangeAndOperate(getSkinnable().multiplayerStateProperty(), state -> { if (state == MultiplayerManager.State.DISCONNECTED) { transitionPane.setContent(disconnectedPane, ContainerAnimations.NONE.getAnimationProducer()); + } else if (state == MultiplayerManager.State.CONNECTING) { + transitionPane.setContent(connectingPane, ContainerAnimations.NONE.getAnimationProducer()); } else if (state == MultiplayerManager.State.MASTER) { transitionPane.setContent(masterPane, ContainerAnimations.NONE.getAnimationProducer()); } else if (state == MultiplayerManager.State.SLAVE) { diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 9754d7fb9..107439b92 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -568,6 +568,8 @@ multiplayer=Multiplayer multiplayer.download=Downloading dependencies for multiplayer multiplayer.download.success=Dependencies initialization succeeded multiplayer.download.failed=Failed to initialize multiplayer, some files cannot be downloaded +multiplayer.exit.before_ready=Multiplayer session failed to create. cato exitcode %d +multiplayer.exit.after_ready=Multiplayer session broken. cato exitcode %d multiplayer.hint=Multiplayer functionality is experimental. Please give feedback. multiplayer.nat=Network Type Detection multiplayer.nat.hint=Network type detection will make it clear whether your network fulfills our requirement for multiplayer mode. @@ -607,6 +609,7 @@ multiplayer.session.join.port.error=Cannot find available local network port for multiplayer.session.members=Room Members multiplayer.session.quit=Quit Room multiplayer.session.username=Username +multiplayer.state.connecting=Connecting multiplayer.state.disconnected=Not created/entered a multiplayer session multiplayer.state.disconnected.hint=Someone should create a multiplayer session, and others join the session to play the game together. multiplayer.state.master=Created room: %1$s, port: %2$d diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 19f8e2302..64c84e9d9 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -568,6 +568,8 @@ multiplayer=多人聯機 multiplayer.download=正在下載相依元件 multiplayer.download.success=多人聯機初始化完成 multiplayer.download.failed=初始化失敗,部分文件未能完成下載 +multiplayer.exit.before_ready=多人聯機房間創建失敗,cato 退出碼 %d +multiplayer.exit.after_ready=多人聯機會話意外退出,退出碼 %d multiplayer.hint=多人聯機功能處於實驗階段,如果有問題請回饋。 multiplayer.nat=網路檢測 multiplayer.nat.hint=執行網路檢測可以讓你更清楚你的網路狀況是否符合聯機功能的需求。不符合聯機功能運行條件的網路狀況將可能導致聯機失敗。 @@ -606,6 +608,7 @@ multiplayer.session.join.port.error=無法找到可用的本地網路埠,請 multiplayer.session.members=房間成員 multiplayer.session.quit=退出房間 multiplayer.session.username=使用者名稱 +multiplayer.state.connecting=連接中 multiplayer.state.disconnected=未創建/加入房間 multiplayer.state.disconnected.hint=多人聯機功能需要先有一位玩家創建房間後,其他玩家加入房間後繼續遊戲。 multiplayer.state.master=你已創建房間:%1$s,埠號 %2$d 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 ff81ddf46..1317b9808 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -568,6 +568,8 @@ multiplayer=多人联机 multiplayer.download=正在下载依赖 multiplayer.download.success=多人联机初始化完成 multiplayer.download.failed=初始化失败,部分文件未能完成下载 +multiplayer.exit.before_ready=多人联机房间创建失败,cato 退出码 %d +multiplayer.exit.after_ready=多人联机会话意外退出,退出码 %d multiplayer.hint=多人联机功能处于实验阶段,如果有问题请反馈。 multiplayer.nat=网络检测 multiplayer.nat.hint=执行网络检测可以让你更清楚你的网络状况是否符合联机功能的需求。不符合联机功能运行条件的网络状况将可能导致联机失败。 @@ -606,6 +608,7 @@ multiplayer.session.join.port.error=无法找到可用的本地网络端口, multiplayer.session.members=房间成员 multiplayer.session.quit=退出房间 multiplayer.session.username=用户名 +multiplayer.state.connecting=连接中 multiplayer.state.disconnected=未创建/加入房间 multiplayer.state.disconnected.hint=多人联机功能需要先有一位玩家创建房间后,其他玩家加入房间后继续游戏。 multiplayer.state.master=你已创建房间:%1$s,端口号 %2$d