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