feat(multiplayer): show state of cato.

This commit is contained in:
huanghongxun 2021-09-21 11:12:04 +08:00
parent 2f5a9a50af
commit 3cf5b97201
6 changed files with 101 additions and 18 deletions

View File

@ -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<CatoExitEvent> onExit = new EventManager<>();
private final EventManager<CatoIdEvent> onIdGenerated = new EventManager<>();
private final EventManager<Event> onPeerConnected = new EventManager<>();
private final String name;
private final State type;
private String id;
CatoSession(String name, Process process, List<String> commands) {
CatoSession(String name, State type, Process process, List<String> 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<CatoIdEvent> onIdGenerated() {
return onIdGenerated;
}
public EventManager<Event> onPeerConnected() {
return onPeerConnected;
}
private static final Pattern TEMP_TOKEN_PATTERN = Pattern.compile("id\\(mix(?<id>\\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
}

View File

@ -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> state = new ReadOnlyObjectWrapper<>(State.fromTitle(i18n("multiplayer"), -1));
private final ObjectProperty<MultiplayerManager.State> multiplayerState = new SimpleObjectProperty<>(MultiplayerManager.State.DISCONNECTED);
private final ReadOnlyStringWrapper token = new ReadOnlyStringWrapper();
private final ReadOnlyObjectWrapper<DiscoveryInfo> natState = new ReadOnlyObjectWrapper<>();
private final ReadOnlyIntegerWrapper port = new ReadOnlyIntegerWrapper(-1);
private final ReadOnlyObjectWrapper<MultiplayerManager.CatoSession> session = new ReadOnlyObjectWrapper<>();
private Consumer<MultiplayerManager.CatoExitEvent> onExit;
private Consumer<MultiplayerManager.CatoIdEvent> onIdGenerated;
private Consumer<Event> 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

View File

@ -125,6 +125,13 @@ public class MultiplayerPageSkin extends SkinBase<MultiplayerPage> {
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<MultiplayerPage> {
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) {

View File

@ -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

View File

@ -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

View File

@ -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