From 8a3cdfb14c820bdf6f30a4c35c89169e8ec29ab4 Mon Sep 17 00:00:00 2001 From: huanghongxun Date: Mon, 27 Sep 2021 22:18:48 +0800 Subject: [PATCH] fix(multiplayer): wrong port. --- .../jackhuang/hmcl/game/LauncherHelper.java | 5 ++- .../hmcl/setting/VersionSetting.java | 15 +++++-- .../ui/multiplayer/MultiplayerClient.java | 13 ++++++ .../ui/multiplayer/MultiplayerManager.java | 33 ++++++++------- .../hmcl/ui/multiplayer/MultiplayerPage.java | 42 ++++++++++++------- .../ui/multiplayer/MultiplayerPageSkin.java | 33 ++++++++++++--- .../ui/multiplayer/MultiplayerServer.java | 24 +++++++++++ .../assets/lang/I18N_zh_CN.properties | 1 - .../java/org/jackhuang/hmcl/util/Range.java | 3 +- 9 files changed, 125 insertions(+), 44 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java index afae52993..c2075bd7f 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/game/LauncherHelper.java @@ -67,7 +67,6 @@ import java.util.function.Consumer; import java.util.logging.Level; import static org.jackhuang.hmcl.setting.ConfigHolder.config; -import static org.jackhuang.hmcl.ui.FXUtils.runInFX; import static org.jackhuang.hmcl.util.Lang.mapOf; import static org.jackhuang.hmcl.util.Logging.LOG; import static org.jackhuang.hmcl.util.Pair.pair; @@ -489,6 +488,10 @@ public final class LauncherHelper { } } + if (!suggested) { + future.complete(javaVersion); + } + return Task.fromCompletableFuture(future); }).thenAcceptAsync(Schedulers.javafx(), javaVersion -> { if (javaVersion == null) { diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java index cdcaf8c94..27b9dc40a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/setting/VersionSetting.java @@ -22,6 +22,7 @@ import com.google.gson.annotations.JsonAdapter; import javafx.beans.InvalidationListener; import javafx.beans.property.*; import org.jackhuang.hmcl.game.*; +import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Task; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.StringUtils; @@ -39,6 +40,8 @@ import java.util.Optional; import java.util.concurrent.CancellationException; import java.util.stream.Collectors; +import static com.jfoenix.concurrency.JFXUtilities.runInFX; + /** * * @author huangyuhui @@ -590,10 +593,12 @@ public final class VersionSetting implements Cloneable { } public Task getJavaVersion(VersionNumber gameVersion, Version version, boolean checkJava) { - return Task.supplyAsync(() -> { + return Task.runAsync(Schedulers.javafx(), () -> { + if (StringUtils.isBlank(getJava())) { + setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom"); + } + }).thenSupplyAsync(() -> { try { - if (StringUtils.isBlank(getJava())) - setJava(StringUtils.isBlank(getJavaDir()) ? "Default" : "Custom"); if ("Default".equals(getJava())) { return JavaVersion.fromCurrentEnvironment(); } else if (isJavaAutoSelected()) { @@ -612,7 +617,9 @@ public final class VersionSetting implements Cloneable { .filter(java -> java.getVersion().equals(getJava())) .collect(Collectors.toList()); if (matchedJava.isEmpty()) { - setJava("Default"); + runInFX(() -> { + setJava("Default"); + }); return JavaVersion.fromCurrentEnvironment(); } else { return matchedJava.stream() diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClient.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClient.java index 34d0a1dff..94e1bb35e 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClient.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerClient.java @@ -32,6 +32,8 @@ public class MultiplayerClient extends Thread { private final String id; private final int port; + private int gamePort; + private final EventManager onConnected = new EventManager<>(); private final EventManager onDisconnected = new EventManager<>(); @@ -43,6 +45,14 @@ public class MultiplayerClient extends Thread { setDaemon(true); } + public void setGamePort(int gamePort) { + this.gamePort = gamePort; + } + + public int getGamePort() { + return gamePort; + } + public EventManager onConnected() { return onConnected; } @@ -53,6 +63,7 @@ public class MultiplayerClient extends Thread { @Override public void run() { + LOG.info("Connecting to 127.0.0.1:" + port); try (Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) { @@ -68,6 +79,7 @@ public class MultiplayerClient extends Thread { } MultiplayerServer.JoinResponse response = JsonUtils.fromNonNullJson(line, MultiplayerServer.JoinResponse.class); + setGamePort(response.getPort()); onConnected.fireEvent(new ConnectedEvent(this, response.getPort())); LOG.fine("Received join response with port " + response.getPort()); @@ -85,6 +97,7 @@ public class MultiplayerClient extends Thread { } catch (IOException | JsonParseException e) { e.printStackTrace(); } finally { + LOG.info("Lost connection to 127.0.0.1:" + port); onDisconnected.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 5632dbf6f..e9d9e0522 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 @@ -18,6 +18,7 @@ package org.jackhuang.hmcl.ui.multiplayer; import com.google.gson.JsonParseException; +import com.google.gson.annotations.SerializedName; import org.jackhuang.hmcl.Metadata; import org.jackhuang.hmcl.event.Event; import org.jackhuang.hmcl.event.EventManager; @@ -62,6 +63,9 @@ public final class MultiplayerManager { static final String CATO_VERSION = "1.0.9"; private static final String CATO_PATH = getCatoPath(); + private static final String REMOTE_ADDRESS = "127.0.0.1"; + private static final String LOCAL_ADDRESS = "127.0.0.1"; + private MultiplayerManager() { } @@ -96,8 +100,8 @@ public final class MultiplayerManager { String[] commands = new String[]{exe.toString(), "--token", StringUtils.isBlank(token) ? "new" : token, "--id", peer, - "--local", String.format("0.0.0.0:%d", localPort), - "--remote", String.format("0.0.0.0:%d", remotePort)}; + "--local", String.format("%s:%d", LOCAL_ADDRESS, localPort), + "--remote", String.format("%s:%d", REMOTE_ADDRESS, remotePort)}; Process process; try { process = new ProcessBuilder() @@ -129,7 +133,7 @@ public final class MultiplayerManager { client.onConnected().register(connectedEvent -> { try { int port = findAvailablePort(); - writer.write(String.format("net add %s 0.0.0.0:%d 0.0.0.0:%d p2p\n", peer, port, connectedEvent.getPort())); + writer.write(String.format("net add %s %s:%d %s:%d p2p\n", peer, LOCAL_ADDRESS, port, REMOTE_ADDRESS, connectedEvent.getPort())); future.complete(session); } catch (IOException e) { future.completeExceptionally(e); @@ -142,18 +146,18 @@ public final class MultiplayerManager { }); } - public static CatoSession createSession(String token, String sessionName, int port) throws IOException { + public static CatoSession createSession(String token, String sessionName, int gamePort) throws IOException { Path exe = getCatoExecutable(); if (!Files.isRegularFile(exe)) { throw new IllegalStateException("Cato file not found"); } - MultiplayerServer server = new MultiplayerServer(port); + MultiplayerServer server = new MultiplayerServer(gamePort); server.startServer(); String[] commands = new String[]{exe.toString(), "--token", StringUtils.isBlank(token) ? "new" : token, - "--allows", String.format("0.0.0.0:%d/0.0.0.0:%d", port, server.getPort())}; + "--allows", String.format("%s:%d/%s:%d", REMOTE_ADDRESS, server.getPort(), REMOTE_ADDRESS, gamePort)}; Process process = new ProcessBuilder() .command(commands) .start(); @@ -291,11 +295,11 @@ public final class MultiplayerManager { return id; } - public String generateInvitationCode(int gamePort, int serverPort) { + public String generateInvitationCode(int serverPort) { if (id == null) { throw new IllegalStateException("id not generated"); } - String json = JsonUtils.GSON.toJson(new Invitation(CATO_VERSION, id, name, gamePort, serverPort)); + String json = JsonUtils.GSON.toJson(new Invitation(CATO_VERSION, id, name, serverPort)); return new String(Base64.getEncoder().encode(json.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8); } @@ -312,7 +316,7 @@ public final class MultiplayerManager { } private static final Pattern TEMP_TOKEN_PATTERN = Pattern.compile("id\\(mix(?\\w+)\\)"); - private static final Pattern PEER_CONNECTED_PATTERN = Pattern.compile("Peer connected"); + private static final Pattern PEER_CONNECTED_PATTERN = Pattern.compile("Connection established"); private static final Pattern LOG_PATTERN = Pattern.compile("(\\[\\d+])\\s+(\\w+)\\s+(\\w+-{0,1}\\w+):\\s(.*)"); } @@ -353,17 +357,18 @@ public final class MultiplayerManager { } public static class Invitation { + @SerializedName("v") private final String version; private final String id; + @SerializedName("n") private final String sessionName; - private final int gamePort; + @SerializedName("p") private final int channelPort; - public Invitation(String version, String id, String sessionName, int gamePort, int channelPort) { + public Invitation(String version, String id, String sessionName, int channelPort) { this.version = version; this.id = id; this.sessionName = sessionName; - this.gamePort = gamePort; this.channelPort = channelPort; } @@ -379,10 +384,6 @@ public final class MultiplayerManager { return sessionName; } - public int getGamePort() { - return gamePort; - } - public int getChannelPort() { return channelPort; } 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 1917de734..f36413b23 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 @@ -21,6 +21,8 @@ import de.javawi.jstun.test.DiscoveryInfo; import de.javawi.jstun.test.DiscoveryTest; import javafx.application.Platform; import javafx.beans.property.*; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; import javafx.scene.control.Control; import javafx.scene.control.Skin; import org.jackhuang.hmcl.event.Event; @@ -49,8 +51,9 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware 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 ReadOnlyIntegerWrapper gamePort = new ReadOnlyIntegerWrapper(-1); private final ReadOnlyObjectWrapper session = new ReadOnlyObjectWrapper<>(); + private final ObservableList clients = FXCollections.observableArrayList(); private Consumer onExit; private Consumer onIdGenerated; @@ -70,6 +73,10 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware return new MultiplayerPageSkin(this); } + public ObservableList getClients() { + return clients; + } + public MultiplayerManager.State getMultiplayerState() { return multiplayerState.get(); } @@ -98,12 +105,12 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware return token.getReadOnlyProperty(); } - public int getPort() { - return port.get(); + public int getGamePort() { + return gamePort.get(); } - public ReadOnlyIntegerProperty portProperty() { - return port.getReadOnlyProperty(); + public ReadOnlyIntegerProperty gamePortProperty() { + return gamePort.getReadOnlyProperty(); } public MultiplayerManager.CatoSession getSession() { @@ -158,11 +165,11 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware } public void copyInvitationCode() { - if (getSession() == null || !getSession().isReady() || port.get() < 0 || getMultiplayerState() != MultiplayerManager.State.MASTER) { + if (getSession() == null || !getSession().isReady() || gamePort.get() < 0 || getMultiplayerState() != MultiplayerManager.State.MASTER) { throw new IllegalStateException("CatoSession not ready"); } - FXUtils.copyText(getSession().generateInvitationCode(port.get(), 0)); + FXUtils.copyText(getSession().generateInvitationCode(getSession().getServer().getPort())); } public void createRoom() { @@ -171,16 +178,22 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware } Controllers.dialog(new CreateMultiplayerRoomDialog((result, resolve, reject) -> { - int port = result.getAd(); + int gamePort = result.getAd(); try { - initCatoSession(MultiplayerManager.createSession(config().getMultiplayerToken(), result.getMotd(), port)); + MultiplayerManager.CatoSession session = MultiplayerManager.createSession(config().getMultiplayerToken(), result.getMotd(), gamePort); + session.getServer().onClientAdded().register(event -> { + runInFX(() -> { + clients.add(event); + }); + }); + initCatoSession(session); } catch (Exception e) { LOG.log(Level.WARNING, "Failed to create session", e); reject.accept(i18n("multiplayer.session.create.error")); return; } - this.port.set(port); + this.gamePort.set(gamePort); setMultiplayerState(MultiplayerManager.State.CONNECTING); resolve.run(); })); @@ -202,7 +215,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware return; } - int localPort; + int localPort; // invitation channel try { localPort = MultiplayerManager.findAvailablePort(); } catch (Exception e) { @@ -211,7 +224,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware } try { - MultiplayerManager.joinSession(config().getMultiplayerToken(), invitation.getVersion(), invitation.getSessionName(), invitation.getId(), invitation.getGamePort(), localPort) + MultiplayerManager.joinSession(config().getMultiplayerToken(), invitation.getVersion(), invitation.getSessionName(), invitation.getId(), invitation.getChannelPort(), localPort) .thenAcceptAsync(session -> { initCatoSession(session); @@ -222,7 +235,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware }); }); - port.set(localPort); + gamePort.set(session.getClient().getGamePort()); setMultiplayerState(MultiplayerManager.State.CONNECTING); resolve.run(); }, Platform::runLater).exceptionally(throwable -> { @@ -270,6 +283,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware onIdGenerated = session.onIdGenerated().registerWeak(this::onCatoIdGenerated); onPeerConnected = session.onPeerConnected().registerWeak(this::onCatoPeerConnected); + this.clients.clear(); this.session.set(session); }); } @@ -282,7 +296,7 @@ public class MultiplayerPage extends Control implements DecoratorPage, PageAware private void clearCatoSession() { this.session.set(null); this.token.set(null); - this.port.set(-1); + this.gamePort.set(-1); this.multiplayerState.set(MultiplayerManager.State.DISCONNECTED); } 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 b704cf4b9..d62a9753d 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 @@ -21,8 +21,10 @@ import com.jfoenix.controls.JFXButton; import com.jfoenix.controls.JFXTextField; import de.javawi.jstun.test.DiscoveryInfo; import javafx.beans.binding.Bindings; +import javafx.collections.ObservableList; import javafx.geometry.Insets; import javafx.geometry.Pos; +import javafx.scene.Node; import javafx.scene.control.Label; import javafx.scene.control.ScrollPane; import javafx.scene.control.SkinBase; @@ -38,6 +40,7 @@ import org.jackhuang.hmcl.ui.animation.TransitionPane; import org.jackhuang.hmcl.ui.construct.*; import org.jackhuang.hmcl.ui.versions.Versions; import org.jackhuang.hmcl.util.javafx.BindingMapping; +import org.jackhuang.hmcl.util.javafx.MappedObservableList; import static org.jackhuang.hmcl.setting.ConfigHolder.config; import static org.jackhuang.hmcl.ui.versions.VersionPage.wrap; @@ -45,6 +48,8 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n; public class MultiplayerPageSkin extends SkinBase { + private ObservableList clients; + /** * Constructor for all SkinBase instances. * @@ -174,9 +179,14 @@ public class MultiplayerPageSkin extends SkinBase { Label label = new Label(i18n("multiplayer.state.master")); label.textProperty().bind(Bindings.createStringBinding(() -> - i18n("multiplayer.state.master", control.getSession() == null ? "" : control.getSession().getName(), control.getPort()), - control.portProperty(), control.sessionProperty())); - masterPane.getChildren().setAll(masterHintPane, label); + i18n("multiplayer.state.master", control.getSession() == null ? "" : control.getSession().getName(), control.getGamePort()), + control.gamePortProperty(), control.sessionProperty())); + + VBox clientsPane = new VBox(8); + clients = MappedObservableList.create(control.getClients(), client -> new ClientItem(client)); + Bindings.bindContent(clientsPane.getChildren(), clients); + + masterPane.getChildren().setAll(masterHintPane, label, clientsPane); } BorderPane slavePane = new BorderPane(); @@ -187,13 +197,13 @@ public class MultiplayerPageSkin extends SkinBase { Label label = new Label(); label.textProperty().bind(Bindings.createStringBinding(() -> - i18n("multiplayer.state.slave", control.getSession() == null ? "" : control.getSession().getName(), "0.0.0.0:" + control.getPort()), - control.sessionProperty(), control.portProperty())); + i18n("multiplayer.state.slave", control.getSession() == null ? "" : control.getSession().getName(), "0.0.0.0:" + control.getGamePort()), + control.sessionProperty(), control.gamePortProperty())); BorderPane.setAlignment(label, Pos.CENTER_LEFT); slavePane.setCenter(label); JFXButton copyButton = new JFXButton(i18n("multiplayer.state.slave.copy")); - copyButton.setOnAction(e -> FXUtils.copyText("0.0.0.0:" + control.getPort())); + copyButton.setOnAction(e -> FXUtils.copyText("0.0.0.0:" + control.getGamePort())); slavePane.setRight(copyButton); } @@ -289,4 +299,15 @@ public class MultiplayerPageSkin extends SkinBase { return i18n("multiplayer.nat.type.unknown"); } } + + private static class ClientItem extends StackPane { + ClientItem(MultiplayerServer.CatoClient client) { + BorderPane pane = new BorderPane(); + pane.setLeft(new Label(client.getUsername())); + + RipplerContainer container = new RipplerContainer(pane); + getChildren().setAll(container); + getStyleClass().add("md-list-cell"); + } + } } diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerServer.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerServer.java index b841ab532..1ffed5d51 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerServer.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/multiplayer/MultiplayerServer.java @@ -18,6 +18,8 @@ package org.jackhuang.hmcl.ui.multiplayer; import com.google.gson.JsonParseException; +import org.jackhuang.hmcl.event.Event; +import org.jackhuang.hmcl.event.EventManager; import org.jackhuang.hmcl.util.Lang; import org.jackhuang.hmcl.util.gson.JsonSubtype; import org.jackhuang.hmcl.util.gson.JsonType; @@ -33,6 +35,8 @@ public class MultiplayerServer extends Thread { private ServerSocket socket; private final int gamePort; + private final EventManager onClientAdded = new EventManager(); + public MultiplayerServer(int gamePort) { this.gamePort = gamePort; @@ -40,6 +44,10 @@ public class MultiplayerServer extends Thread { setDaemon(true); } + public EventManager onClientAdded() { + return onClientAdded; + } + public void startServer() throws IOException { if (socket != null) { throw new IllegalStateException("MultiplayerServer already started"); @@ -59,6 +67,7 @@ public class MultiplayerServer extends Thread { @Override public void run() { + LOG.info("Multiplayer Server listening 127.0.0.1:" + socket.getLocalPort()); try { while (!isInterrupted()) { Socket clientSocket = socket.accept(); @@ -117,6 +126,8 @@ public class MultiplayerServer extends Thread { LOG.fine("Received join request with clientVersion=" + clientVersion + ", id=" + username); writer.write(JsonUtils.GSON.toJson(new JoinResponse(server.gamePort))); + + server.onClientAdded.fireEvent(new CatoClient(server, username)); } } @@ -171,4 +182,17 @@ public class MultiplayerServer extends Thread { return timestamp; } } + + public static class CatoClient extends Event { + private final String username; + + public CatoClient(Object source, String username) { + super(source); + this.username = username; + } + + public String getUsername() { + return username; + } + } } 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 2cab61e92..bb9c600cf 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -770,7 +770,6 @@ settings.game.java_directory=Java 路径 settings.game.java_directory.auto=自动选择合适的 Java settings.game.java_directory.bit=,%s 位 settings.game.java_directory.choose=选择 Java 路径 -settings.game. settings.game.management=管理 settings.game.working_directory=版本隔离(修改后请自行移动相关游戏文件,如存档模组配置等) settings.game.working_directory.choose=选择运行路径 diff --git a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Range.java b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Range.java index aa1feb5a1..495e762de 100644 --- a/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Range.java +++ b/HMCLCore/src/main/java/org/jackhuang/hmcl/util/Range.java @@ -20,8 +20,7 @@ package org.jackhuang.hmcl.util; import java.util.Comparator; import java.util.Objects; -public class Range { - +public final class Range { @SuppressWarnings({"rawtypes", "unchecked"}) private enum ComparableComparator implements Comparator {