feat(multiplayer): an option for auto allowing all join requests. Closes #1093.
This commit is contained in:
@@ -23,6 +23,8 @@ import com.jfoenix.controls.JFXTextField;
|
|||||||
import com.jfoenix.validation.base.ValidatorBase;
|
import com.jfoenix.validation.base.ValidatorBase;
|
||||||
import javafx.beans.binding.Bindings;
|
import javafx.beans.binding.Bindings;
|
||||||
import javafx.beans.binding.BooleanBinding;
|
import javafx.beans.binding.BooleanBinding;
|
||||||
|
import javafx.beans.property.SimpleStringProperty;
|
||||||
|
import javafx.beans.property.StringProperty;
|
||||||
import javafx.geometry.Insets;
|
import javafx.geometry.Insets;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.layout.ColumnConstraints;
|
import javafx.scene.layout.ColumnConstraints;
|
||||||
@@ -67,8 +69,8 @@ public class PromptDialogPane extends DialogPane {
|
|||||||
}
|
}
|
||||||
bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty()));
|
bindings.add(Bindings.createBooleanBinding(textField::validate, textField.textProperty()));
|
||||||
|
|
||||||
if (StringUtils.isNotBlank(question.question)) {
|
if (StringUtils.isNotBlank(question.question.get())) {
|
||||||
body.addRow(rowIndex++, new Label(question.question), textField);
|
body.addRow(rowIndex++, new Label(question.question.get()), textField);
|
||||||
} else {
|
} else {
|
||||||
GridPane.setColumnSpan(textField, 2);
|
GridPane.setColumnSpan(textField, 2);
|
||||||
body.addRow(rowIndex++, textField);
|
body.addRow(rowIndex++, textField);
|
||||||
@@ -82,7 +84,7 @@ public class PromptDialogPane extends DialogPane {
|
|||||||
HBox.setMargin(checkBox, new Insets(0, 0, 0, -10));
|
HBox.setMargin(checkBox, new Insets(0, 0, 0, -10));
|
||||||
checkBox.setSelected(((Builder.BooleanQuestion) question).value);
|
checkBox.setSelected(((Builder.BooleanQuestion) question).value);
|
||||||
checkBox.selectedProperty().addListener((a, b, newValue) -> ((Builder.BooleanQuestion) question).value = newValue);
|
checkBox.selectedProperty().addListener((a, b, newValue) -> ((Builder.BooleanQuestion) question).value = newValue);
|
||||||
checkBox.setText(question.question);
|
checkBox.setText(question.question.get());
|
||||||
body.addRow(rowIndex++, hBox);
|
body.addRow(rowIndex++, hBox);
|
||||||
} else if (question instanceof Builder.CandidatesQuestion) {
|
} else if (question instanceof Builder.CandidatesQuestion) {
|
||||||
JFXComboBox<String> comboBox = new JFXComboBox<>();
|
JFXComboBox<String> comboBox = new JFXComboBox<>();
|
||||||
@@ -90,8 +92,8 @@ public class PromptDialogPane extends DialogPane {
|
|||||||
comboBox.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) ->
|
comboBox.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) ->
|
||||||
((Builder.CandidatesQuestion) question).value = newValue.intValue());
|
((Builder.CandidatesQuestion) question).value = newValue.intValue());
|
||||||
comboBox.getSelectionModel().select(0);
|
comboBox.getSelectionModel().select(0);
|
||||||
if (StringUtils.isNotBlank(question.question)) {
|
if (StringUtils.isNotBlank(question.question.get())) {
|
||||||
body.addRow(rowIndex++, new Label(question.question), comboBox);
|
body.addRow(rowIndex++, new Label(question.question.get()), comboBox);
|
||||||
} else {
|
} else {
|
||||||
GridPane.setColumnSpan(comboBox, 2);
|
GridPane.setColumnSpan(comboBox, 2);
|
||||||
body.addRow(rowIndex++, comboBox);
|
body.addRow(rowIndex++, comboBox);
|
||||||
@@ -99,7 +101,7 @@ public class PromptDialogPane extends DialogPane {
|
|||||||
} else if (question instanceof Builder.HintQuestion) {
|
} else if (question instanceof Builder.HintQuestion) {
|
||||||
HintPane pane = new HintPane();
|
HintPane pane = new HintPane();
|
||||||
GridPane.setColumnSpan(pane, 2);
|
GridPane.setColumnSpan(pane, 2);
|
||||||
pane.setText(question.question);
|
pane.textProperty().bind(question.question);
|
||||||
body.addRow(rowIndex++, pane);
|
body.addRow(rowIndex++, pane);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -142,16 +144,28 @@ public class PromptDialogPane extends DialogPane {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static class Question<T> {
|
public static class Question<T> {
|
||||||
public final String question;
|
public final StringProperty question = new SimpleStringProperty();
|
||||||
protected T value;
|
protected T value;
|
||||||
|
|
||||||
public Question(String question) {
|
public Question(String question) {
|
||||||
this.question = question;
|
this.question.set(question);
|
||||||
}
|
}
|
||||||
|
|
||||||
public T getValue() {
|
public T getValue() {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getQuestion() {
|
||||||
|
return question.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public StringProperty questionProperty() {
|
||||||
|
return question;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuestion(String question) {
|
||||||
|
this.question.set(question);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class HintQuestion extends Question<Void> {
|
public static class HintQuestion extends Question<Void> {
|
||||||
|
|||||||
@@ -17,6 +17,9 @@
|
|||||||
*/
|
*/
|
||||||
package org.jackhuang.hmcl.ui.multiplayer;
|
package org.jackhuang.hmcl.ui.multiplayer;
|
||||||
|
|
||||||
|
import com.jfoenix.controls.JFXCheckBox;
|
||||||
|
import javafx.beans.property.BooleanProperty;
|
||||||
|
import javafx.beans.property.SimpleBooleanProperty;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ProgressIndicator;
|
import javafx.scene.control.ProgressIndicator;
|
||||||
import javafx.scene.layout.ColumnConstraints;
|
import javafx.scene.layout.ColumnConstraints;
|
||||||
@@ -35,12 +38,13 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
|||||||
|
|
||||||
public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAware {
|
public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAware {
|
||||||
|
|
||||||
private final FutureCallback<LocalServerDetector.PingResponse> callback;
|
private final FutureCallback<CreationRequest> callback;
|
||||||
private final LocalServerDetector lanServerDetectorThread;
|
private final LocalServerDetector lanServerDetectorThread;
|
||||||
|
private final BooleanProperty allowAllJoinRequests = new SimpleBooleanProperty(true);
|
||||||
|
|
||||||
private LocalServerDetector.PingResponse server;
|
private LocalServerDetector.PingResponse server;
|
||||||
|
|
||||||
CreateMultiplayerRoomDialog(FutureCallback<LocalServerDetector.PingResponse> callback) {
|
CreateMultiplayerRoomDialog(FutureCallback<CreationRequest> callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
|
|
||||||
setTitle(i18n("multiplayer.session.create"));
|
setTitle(i18n("multiplayer.session.create"));
|
||||||
@@ -70,6 +74,11 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa
|
|||||||
portLabel.setText(i18n("multiplayer.nat.testing"));
|
portLabel.setText(i18n("multiplayer.nat.testing"));
|
||||||
body.addRow(2, new Label(i18n("multiplayer.session.create.port")), portLabel);
|
body.addRow(2, new Label(i18n("multiplayer.session.create.port")), portLabel);
|
||||||
|
|
||||||
|
JFXCheckBox allowAllJoinRequestsCheckBox = new JFXCheckBox(i18n("multiplayer.session.create.join.allow"));
|
||||||
|
allowAllJoinRequestsCheckBox.selectedProperty().bindBidirectional(allowAllJoinRequests);
|
||||||
|
GridPane.setColumnSpan(allowAllJoinRequestsCheckBox, 2);
|
||||||
|
body.addRow(3, allowAllJoinRequestsCheckBox);
|
||||||
|
|
||||||
setValid(false);
|
setValid(false);
|
||||||
|
|
||||||
JFXHyperlink noinLink = new JFXHyperlink();
|
JFXHyperlink noinLink = new JFXHyperlink();
|
||||||
@@ -102,7 +111,10 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa
|
|||||||
protected void onAccept() {
|
protected void onAccept() {
|
||||||
setLoading();
|
setLoading();
|
||||||
|
|
||||||
callback.call(Objects.requireNonNull(server), () -> {
|
callback.call(new CreationRequest(
|
||||||
|
Objects.requireNonNull(server),
|
||||||
|
allowAllJoinRequests.get()
|
||||||
|
), () -> {
|
||||||
runInFX(this::onSuccess);
|
runInFX(this::onSuccess);
|
||||||
}, msg -> {
|
}, msg -> {
|
||||||
runInFX(() -> onFailure(msg));
|
runInFX(() -> onFailure(msg));
|
||||||
@@ -121,4 +133,21 @@ public class CreateMultiplayerRoomDialog extends DialogPane implements DialogAwa
|
|||||||
lanServerDetectorThread.interrupt();
|
lanServerDetectorThread.interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class CreationRequest {
|
||||||
|
private final LocalServerDetector.PingResponse server;
|
||||||
|
private final boolean allowAllJoinRequests;
|
||||||
|
|
||||||
|
public CreationRequest(LocalServerDetector.PingResponse server, boolean allowAllJoinRequests) {
|
||||||
|
this.server = server;
|
||||||
|
this.allowAllJoinRequests = allowAllJoinRequests;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalServerDetector.PingResponse getServer() {
|
||||||
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAllowAllJoinRequests() {
|
||||||
|
return allowAllJoinRequests;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ public final class MultiplayerManager {
|
|||||||
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH);
|
return Metadata.HMCL_DIRECTORY.resolve("libraries").resolve(CATO_PATH);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CompletableFuture<CatoSession> joinSession(String token, String version, String sessionName, String peer, Mode mode, int remotePort, int localPort) throws IncompatibleCatoVersionException {
|
public static CompletableFuture<CatoSession> joinSession(String token, String version, String sessionName, String peer, Mode mode, int remotePort, int localPort, JoinSessionHandler handler) throws IncompatibleCatoVersionException {
|
||||||
if (!CATO_VERSION.equals(version)) {
|
if (!CATO_VERSION.equals(version)) {
|
||||||
throw new IncompatibleCatoVersionException(version, CATO_VERSION);
|
throw new IncompatibleCatoVersionException(version, CATO_VERSION);
|
||||||
}
|
}
|
||||||
@@ -133,6 +133,16 @@ public final class MultiplayerManager {
|
|||||||
MultiplayerClient client = new MultiplayerClient(session.getId(), localPort);
|
MultiplayerClient client = new MultiplayerClient(session.getId(), localPort);
|
||||||
session.addRelatedThread(client);
|
session.addRelatedThread(client);
|
||||||
session.setClient(client);
|
session.setClient(client);
|
||||||
|
|
||||||
|
if (handler != null) {
|
||||||
|
handler.onWaitingForJoinResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
TimerTask task = Lang.setTimeout(() -> {
|
||||||
|
future.completeExceptionally(new JoinRequestTimeoutException());
|
||||||
|
session.stop();
|
||||||
|
}, 30 * 1000);
|
||||||
|
|
||||||
client.onConnected().register(connectedEvent -> {
|
client.onConnected().register(connectedEvent -> {
|
||||||
try {
|
try {
|
||||||
int port = findAvailablePort();
|
int port = findAvailablePort();
|
||||||
@@ -148,10 +158,12 @@ public final class MultiplayerManager {
|
|||||||
future.completeExceptionally(e);
|
future.completeExceptionally(e);
|
||||||
session.stop();
|
session.stop();
|
||||||
}
|
}
|
||||||
|
task.cancel();
|
||||||
});
|
});
|
||||||
client.onKicked().register(kickedEvent -> {
|
client.onKicked().register(kickedEvent -> {
|
||||||
future.completeExceptionally(new CancellationException());
|
future.completeExceptionally(new CancellationException());
|
||||||
session.stop();
|
session.stop();
|
||||||
|
task.cancel();
|
||||||
});
|
});
|
||||||
client.start();
|
client.start();
|
||||||
});
|
});
|
||||||
@@ -160,7 +172,7 @@ public final class MultiplayerManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CatoSession createSession(String token, String sessionName, int gamePort) throws IOException {
|
public static CatoSession createSession(String token, String sessionName, int gamePort, boolean allowAllJoinRequests) throws IOException {
|
||||||
Path exe = getCatoExecutable();
|
Path exe = getCatoExecutable();
|
||||||
if (!Files.isRegularFile(exe)) {
|
if (!Files.isRegularFile(exe)) {
|
||||||
throw new IllegalStateException("Cato file not found");
|
throw new IllegalStateException("Cato file not found");
|
||||||
@@ -170,7 +182,7 @@ public final class MultiplayerManager {
|
|||||||
throw new CatoAlreadyStartedException();
|
throw new CatoAlreadyStartedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
MultiplayerServer server = new MultiplayerServer(gamePort);
|
MultiplayerServer server = new MultiplayerServer(gamePort, allowAllJoinRequests);
|
||||||
server.startServer();
|
server.startServer();
|
||||||
|
|
||||||
String[] commands = new String[]{exe.toString(),
|
String[] commands = new String[]{exe.toString(),
|
||||||
@@ -419,6 +431,10 @@ public final class MultiplayerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface JoinSessionHandler {
|
||||||
|
void onWaitingForJoinResponse();
|
||||||
|
}
|
||||||
|
|
||||||
public static class IncompatibleCatoVersionException extends Exception {
|
public static class IncompatibleCatoVersionException extends Exception {
|
||||||
private final String expected;
|
private final String expected;
|
||||||
private final String actual;
|
private final String actual;
|
||||||
@@ -448,4 +464,7 @@ public final class MultiplayerManager {
|
|||||||
|
|
||||||
public static class CatoAlreadyStartedException extends RuntimeException {
|
public static class CatoAlreadyStartedException extends RuntimeException {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class JoinRequestTimeoutException extends RuntimeException {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,9 +210,9 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
|||||||
}
|
}
|
||||||
|
|
||||||
Controllers.dialog(new CreateMultiplayerRoomDialog((result, resolve, reject) -> {
|
Controllers.dialog(new CreateMultiplayerRoomDialog((result, resolve, reject) -> {
|
||||||
int gamePort = result.getAd();
|
int gamePort = result.getServer().getAd();
|
||||||
try {
|
try {
|
||||||
MultiplayerManager.CatoSession session = MultiplayerManager.createSession(globalConfig().getMultiplayerToken(), result.getMotd(), gamePort);
|
MultiplayerManager.CatoSession session = MultiplayerManager.createSession(globalConfig().getMultiplayerToken(), result.getServer().getMotd(), gamePort, result.isAllowAllJoinRequests());
|
||||||
session.getServer().setOnClientAdding((client, resolveClient, rejectClient) -> {
|
session.getServer().setOnClientAdding((client, resolveClient, rejectClient) -> {
|
||||||
runInFX(() -> {
|
runInFX(() -> {
|
||||||
Controllers.confirm(i18n("multiplayer.session.create.join.prompt", client.getUsername()), i18n("multiplayer.session.create.join"), MessageDialogPane.MessageType.INFO,
|
Controllers.confirm(i18n("multiplayer.session.create.join.prompt", client.getUsername()), i18n("multiplayer.session.create.join"), MessageDialogPane.MessageType.INFO,
|
||||||
@@ -251,6 +251,8 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
|||||||
}
|
}
|
||||||
|
|
||||||
Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.join"), (result, resolve, reject) -> {
|
Controllers.prompt(new PromptDialogPane.Builder(i18n("multiplayer.session.join"), (result, resolve, reject) -> {
|
||||||
|
PromptDialogPane.Builder.HintQuestion hintQuestion = (PromptDialogPane.Builder.HintQuestion) result.get(0);
|
||||||
|
|
||||||
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 {
|
||||||
@@ -279,7 +281,12 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
|||||||
? MultiplayerManager.Mode.RELAY
|
? MultiplayerManager.Mode.RELAY
|
||||||
: MultiplayerManager.Mode.P2P,
|
: MultiplayerManager.Mode.P2P,
|
||||||
invitation.getChannelPort(),
|
invitation.getChannelPort(),
|
||||||
localPort)
|
localPort, new MultiplayerManager.JoinSessionHandler() {
|
||||||
|
@Override
|
||||||
|
public void onWaitingForJoinResponse() {
|
||||||
|
hintQuestion.setQuestion(i18n("multiplayer.session.join.wait"));
|
||||||
|
}
|
||||||
|
})
|
||||||
.thenAcceptAsync(session -> {
|
.thenAcceptAsync(session -> {
|
||||||
initCatoSession(session);
|
initCatoSession(session);
|
||||||
|
|
||||||
@@ -314,6 +321,10 @@ public class MultiplayerPage extends DecoratorAnimatedPage implements DecoratorP
|
|||||||
LOG.info("Cato already started");
|
LOG.info("Cato already started");
|
||||||
reject.accept(i18n("multiplayer.session.error.already_started"));
|
reject.accept(i18n("multiplayer.session.error.already_started"));
|
||||||
return null;
|
return null;
|
||||||
|
} else if (throwable instanceof MultiplayerManager.JoinRequestTimeoutException) {
|
||||||
|
LOG.info("Cato already started");
|
||||||
|
reject.accept(i18n("multiplayer.session.join.wait_timeout"));
|
||||||
|
return null;
|
||||||
} else {
|
} else {
|
||||||
LOG.log(Level.WARNING, "Failed to join sessoin");
|
LOG.log(Level.WARNING, "Failed to join sessoin");
|
||||||
reject.accept(i18n("multiplayer.session.join.error"));
|
reject.accept(i18n("multiplayer.session.join.error"));
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
|||||||
public class MultiplayerServer extends Thread {
|
public class MultiplayerServer extends Thread {
|
||||||
private ServerSocket socket;
|
private ServerSocket socket;
|
||||||
private final int gamePort;
|
private final int gamePort;
|
||||||
|
private final boolean allowAllJoinRequests;
|
||||||
|
|
||||||
private FutureCallback<CatoClient> onClientAdding;
|
private FutureCallback<CatoClient> onClientAdding;
|
||||||
private final EventManager<MultiplayerChannel.CatoClient> onClientAdded = new EventManager<>();
|
private final EventManager<MultiplayerChannel.CatoClient> onClientAdded = new EventManager<>();
|
||||||
@@ -46,8 +47,9 @@ public class MultiplayerServer extends Thread {
|
|||||||
private final Map<String, Endpoint> clients = new ConcurrentHashMap<>();
|
private final Map<String, Endpoint> clients = new ConcurrentHashMap<>();
|
||||||
private final Map<String, Endpoint> nameClientMap = new ConcurrentHashMap<>();
|
private final Map<String, Endpoint> nameClientMap = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public MultiplayerServer(int gamePort) {
|
public MultiplayerServer(int gamePort, boolean allowAllJoinRequests) {
|
||||||
this.gamePort = gamePort;
|
this.gamePort = gamePort;
|
||||||
|
this.allowAllJoinRequests = allowAllJoinRequests;
|
||||||
|
|
||||||
setName("MultiplayerServer");
|
setName("MultiplayerServer");
|
||||||
setDaemon(true);
|
setDaemon(true);
|
||||||
@@ -146,7 +148,7 @@ public class MultiplayerServer extends Thread {
|
|||||||
nameClientMap.put(clientName, endpoint);
|
nameClientMap.put(clientName, endpoint);
|
||||||
onClientAdded.fireEvent(catoClient);
|
onClientAdded.fireEvent(catoClient);
|
||||||
|
|
||||||
if (onClientAdding != null) {
|
if (onClientAdding != null && !allowAllJoinRequests) {
|
||||||
onClientAdding.call(catoClient, () -> {
|
onClientAdding.call(catoClient, () -> {
|
||||||
try {
|
try {
|
||||||
endpoint.write(new JoinResponse(gamePort));
|
endpoint.write(new JoinResponse(gamePort));
|
||||||
@@ -168,6 +170,9 @@ public class MultiplayerServer extends Thread {
|
|||||||
LOG.log(Level.WARNING, "Failed to send kick response.", e);
|
LOG.log(Level.WARNING, "Failed to send kick response.", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Allow all join requests.
|
||||||
|
endpoint.write(new JoinResponse(gamePort));
|
||||||
}
|
}
|
||||||
} else if (request instanceof KeepAliveRequest) {
|
} else if (request instanceof KeepAliveRequest) {
|
||||||
endpoint.write(new KeepAliveResponse(System.currentTimeMillis()));
|
endpoint.write(new KeepAliveResponse(System.currentTimeMillis()));
|
||||||
|
|||||||
@@ -652,6 +652,7 @@ multiplayer.session.create=Create Session
|
|||||||
multiplayer.session.create.error=Failed to create multiplayer session.
|
multiplayer.session.create.error=Failed to create multiplayer session.
|
||||||
multiplayer.session.create.hint=Before creating multiplayer session, you must click "Open LAN Server" in running game, and type the port displayed in game in the blank below.
|
multiplayer.session.create.hint=Before creating multiplayer session, you must click "Open LAN Server" in running game, and type the port displayed in game in the blank below.
|
||||||
multiplayer.session.create.join=Connection request
|
multiplayer.session.create.join=Connection request
|
||||||
|
multiplayer.session.create.join.allow=Accepts all join requests (When disabled, you need to accept join requests manually, so you can prevent from attacks.)
|
||||||
multiplayer.session.create.join.prompt=Player %s wants to join the multiplayer session. Accept?
|
multiplayer.session.create.join.prompt=Player %s wants to join the multiplayer session. Accept?
|
||||||
multiplayer.session.create.members=Members
|
multiplayer.session.create.members=Members
|
||||||
multiplayer.session.create.members.kick=Kick
|
multiplayer.session.create.members.kick=Kick
|
||||||
@@ -675,6 +676,8 @@ multiplayer.session.join.kicked=You have been kicked by the session holder. You
|
|||||||
multiplayer.session.join.lost_connection=Lost connection with the multiplayer session. Maybe the session is destroyed by the creator, or you cannot establish connection with this session.
|
multiplayer.session.join.lost_connection=Lost connection with the multiplayer session. Maybe the session is destroyed by the creator, or you cannot establish connection with this session.
|
||||||
multiplayer.session.join.port.error=Cannot find available local network port for listening. Please ensure that HMCL has the permission to listen on a port.
|
multiplayer.session.join.port.error=Cannot find available local network port for listening. Please ensure that HMCL has the permission to listen on a port.
|
||||||
multiplayer.session.join.rejected=Your connection is rejected by the session holder.
|
multiplayer.session.join.rejected=Your connection is rejected by the session holder.
|
||||||
|
multiplayer.session.join.wait=Waiting for acceptation.
|
||||||
|
multiplayer.session.join.wait_timeout=The session holder does not accept your join request in time.
|
||||||
multiplayer.session.members=Room Members
|
multiplayer.session.members=Room Members
|
||||||
multiplayer.session.quit=Quit Room
|
multiplayer.session.quit=Quit Room
|
||||||
multiplayer.session.quit.warning=After quiting room, you will lost the connection with the server. Continue?
|
multiplayer.session.quit.warning=After quiting room, you will lost the connection with the server. Continue?
|
||||||
|
|||||||
@@ -652,6 +652,7 @@ multiplayer.session.create=創建房間
|
|||||||
multiplayer.session.create.error=創建聯機房間失敗。
|
multiplayer.session.create.error=創建聯機房間失敗。
|
||||||
multiplayer.session.create.hint=創建聯機房間前,你需要先在正在運行的遊戲內的遊戲菜單中選擇 對區域網路開放 選項,然後在下方的輸入框中輸入遊戲內提示的埠號(通常是 5 位的數字)
|
multiplayer.session.create.hint=創建聯機房間前,你需要先在正在運行的遊戲內的遊戲菜單中選擇 對區域網路開放 選項,然後在下方的輸入框中輸入遊戲內提示的埠號(通常是 5 位的數字)
|
||||||
multiplayer.session.create.join=連接申請
|
multiplayer.session.create.join=連接申請
|
||||||
|
multiplayer.session.create.join.allow=自動接受所有連接申請(不啟用此選項時,你需要手動同意申請,以避免不相關人士誤連你的伺服器)
|
||||||
multiplayer.session.create.join.prompt=玩家 %s 申請加入多人聯機房間,是否接受?
|
multiplayer.session.create.join.prompt=玩家 %s 申請加入多人聯機房間,是否接受?
|
||||||
multiplayer.session.create.members=成員
|
multiplayer.session.create.members=成員
|
||||||
multiplayer.session.create.members.kick=踢出房間
|
multiplayer.session.create.members.kick=踢出房間
|
||||||
@@ -674,6 +675,8 @@ multiplayer.session.join.kicked=你已經被房主踢出房間,你將與房間
|
|||||||
multiplayer.session.join.lost_connection=你已與房間失去連接。這可能意味著房主已經解散房間,或者你無法連接至房間。
|
multiplayer.session.join.lost_connection=你已與房間失去連接。這可能意味著房主已經解散房間,或者你無法連接至房間。
|
||||||
multiplayer.session.join.port.error=無法找到可用的本地網路埠,請確保 HMCL 擁有綁定本地埠的權限。
|
multiplayer.session.join.port.error=無法找到可用的本地網路埠,請確保 HMCL 擁有綁定本地埠的權限。
|
||||||
multiplayer.session.join.rejected=你被房主拒絕連接。
|
multiplayer.session.join.rejected=你被房主拒絕連接。
|
||||||
|
multiplayer.session.join.wait=等待對方同意加入申請。
|
||||||
|
multiplayer.session.join.wait_timeout=對方未能即時同意你的加入申請
|
||||||
multiplayer.session.members=房間成員
|
multiplayer.session.members=房間成員
|
||||||
multiplayer.session.quit=退出房間
|
multiplayer.session.quit=退出房間
|
||||||
multiplayer.session.quit.warning=退出房間後,你將會與伺服器斷開連接,是否繼續?
|
multiplayer.session.quit.warning=退出房間後,你將會與伺服器斷開連接,是否繼續?
|
||||||
|
|||||||
@@ -652,6 +652,7 @@ multiplayer.session.create=创建房间
|
|||||||
multiplayer.session.create.error=创建联机房间失败。
|
multiplayer.session.create.error=创建联机房间失败。
|
||||||
multiplayer.session.create.hint=创建联机房间前,你需要先在正在运行的游戏内的游戏菜单中选择 对局域网开放 选项,然后在下方的输入框中确认游戏内提示的端口号(通常是 5 位的数字)
|
multiplayer.session.create.hint=创建联机房间前,你需要先在正在运行的游戏内的游戏菜单中选择 对局域网开放 选项,然后在下方的输入框中确认游戏内提示的端口号(通常是 5 位的数字)
|
||||||
multiplayer.session.create.join=连接申请
|
multiplayer.session.create.join=连接申请
|
||||||
|
multiplayer.session.create.join.allow=自动接受所有连接申请(不启用此选项时,你需要手动同意申请,以避免不相关人士误连你的服务器)
|
||||||
multiplayer.session.create.join.prompt=玩家 %s 申请加入多人联机房间,是否接受?
|
multiplayer.session.create.join.prompt=玩家 %s 申请加入多人联机房间,是否接受?
|
||||||
multiplayer.session.create.members=成员
|
multiplayer.session.create.members=成员
|
||||||
multiplayer.session.create.members.kick=踢出房间
|
multiplayer.session.create.members.kick=踢出房间
|
||||||
@@ -674,6 +675,8 @@ multiplayer.session.join.kicked=你已经被房主踢出房间,你将与房间
|
|||||||
multiplayer.session.join.lost_connection=你已与房间失去连接。这可能意味着房主已经解散房间,或者你无法连接至房间。
|
multiplayer.session.join.lost_connection=你已与房间失去连接。这可能意味着房主已经解散房间,或者你无法连接至房间。
|
||||||
multiplayer.session.join.port.error=无法找到可用的本地网络端口,请确保 HMCL 拥有绑定本地端口的权限。
|
multiplayer.session.join.port.error=无法找到可用的本地网络端口,请确保 HMCL 拥有绑定本地端口的权限。
|
||||||
multiplayer.session.join.rejected=你被房主拒绝连接。
|
multiplayer.session.join.rejected=你被房主拒绝连接。
|
||||||
|
multiplayer.session.join.wait=等待对方同意加入申请。
|
||||||
|
multiplayer.session.join.wait_timeout=对方未能即时同意你的加入申请
|
||||||
multiplayer.session.members=房间成员
|
multiplayer.session.members=房间成员
|
||||||
multiplayer.session.quit=退出房间
|
multiplayer.session.quit=退出房间
|
||||||
multiplayer.session.quit.warning=退出房间后,你将会与服务器断开连接,是否继续?
|
multiplayer.session.quit.warning=退出房间后,你将会与服务器断开连接,是否继续?
|
||||||
|
|||||||
@@ -357,6 +357,22 @@ public final class Lang {
|
|||||||
return () -> iterator;
|
return () -> iterator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Timer timer;
|
||||||
|
|
||||||
|
public static synchronized TimerTask setTimeout(Runnable runnable, long delayMs) {
|
||||||
|
if (timer == null) {
|
||||||
|
timer = new Timer();
|
||||||
|
}
|
||||||
|
TimerTask task = new TimerTask() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timer.schedule(task, delayMs);
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is a useful function to prevent exceptions being eaten when using CompletableFuture.
|
* This is a useful function to prevent exceptions being eaten when using CompletableFuture.
|
||||||
* You can write:
|
* You can write:
|
||||||
|
|||||||
Reference in New Issue
Block a user