modding: improve code and performance of callback event invoker, convert ServerListCell to kotlin

This commit is contained in:
Bixilon 2021-04-21 00:42:33 +02:00
parent 31904238aa
commit 0d07a308c8
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
11 changed files with 521 additions and 628 deletions

View File

@ -39,7 +39,7 @@ public class Server {
private int desiredVersion;
private byte[] favicon;
private StatusConnection lastPing;
private boolean readOnly;
private boolean temporary;
private ServerListCell cell;
public Server(int id, ChatComponent name, String address, int desiredVersion, byte[] favicon) {
@ -67,7 +67,7 @@ public class Server {
this.name = ChatComponent.Companion.valueOf(String.format("LAN Server #%d", LANServerListener.getServerMap().size()));
this.address = address.toString();
this.desiredVersion = -1; // Automatic
this.readOnly = true;
this.temporary = true;
}
public static int getNextServerId() {
@ -96,7 +96,7 @@ public class Server {
}
public void saveToConfig() {
if (isReadOnly()) {
if (isTemporary()) {
return;
}
Minosoft.getConfig().getConfig().getServer().getEntries().put(this.getId(), this);
@ -104,7 +104,7 @@ public class Server {
}
public void delete() {
if (isReadOnly()) {
if (isTemporary()) {
return;
}
Minosoft.getConfig().getConfig().getServer().getEntries().remove(this.getId());
@ -197,8 +197,8 @@ public class Server {
return Base64.getEncoder().encodeToString(this.favicon);
}
public boolean isReadOnly() {
return this.readOnly;
public boolean isTemporary() {
return this.temporary;
}
public ServerListCell getCell() {

View File

@ -1,451 +0,0 @@
/*
* Minosoft
* Copyright (C) 2020 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.main;
import com.jfoenix.controls.JFXAlert;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialogLayout;
import de.bixilon.minosoft.Minosoft;
import de.bixilon.minosoft.data.locale.LocaleManager;
import de.bixilon.minosoft.data.locale.Strings;
import de.bixilon.minosoft.data.mappings.ResourceLocation;
import de.bixilon.minosoft.data.mappings.versions.Version;
import de.bixilon.minosoft.data.mappings.versions.Versions;
import de.bixilon.minosoft.data.player.tab.PingBars;
import de.bixilon.minosoft.data.text.ChatComponent;
import de.bixilon.minosoft.modding.event.CallbackEventInvoker;
import de.bixilon.minosoft.modding.event.events.ConnectionStateChangeEvent;
import de.bixilon.minosoft.modding.event.events.ServerListPongEvent;
import de.bixilon.minosoft.modding.event.events.ServerListStatusArriveEvent;
import de.bixilon.minosoft.protocol.network.connection.PlayConnection;
import de.bixilon.minosoft.protocol.ping.ForgeModInfo;
import de.bixilon.minosoft.protocol.ping.ServerListPing;
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition;
import de.bixilon.minosoft.util.CountUpAndDownLatch;
import de.bixilon.minosoft.util.logging.Log;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.scene.text.TextFlow;
import javafx.stage.Modality;
import java.io.IOException;
import java.net.URL;
import java.util.Arrays;
import java.util.ResourceBundle;
public class ServerListCell extends ListCell<Server> implements Initializable {
public static final ListView<Server> SERVER_LIST_VIEW = new ListView<>();
public HBox hBox;
public GridPane root;
public ImageView faviconField;
public TextFlow nameField;
public TextFlow motdField;
public Label versionField;
public Label playersField;
public Label brandField;
public MenuItem optionsConnect;
public MenuItem optionsShowInfo;
public MenuItem optionsEdit;
public MenuItem optionsRefresh;
public MenuItem optionsSessions;
public MenuItem optionsDelete;
public MenuButton optionsMenu;
public Label pingField;
private boolean canConnect;
private Server server;
public static ServerListCell newInstance() {
FXMLLoader loader = new FXMLLoader(Minosoft.MINOSOFT_ASSETS_MANAGER.getAssetURL(new ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "layout/cells/server.fxml")));
try {
loader.load();
return loader.getController();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
@Override
public void initialize(URL url, ResourceBundle rb) {
updateSelected(false);
setGraphic(this.root);
// change locale
this.optionsConnect.setText(LocaleManager.translate(Strings.SERVER_ACTION_CONNECT));
this.optionsShowInfo.setText(LocaleManager.translate(Strings.SERVER_ACTION_SHOW_INFO));
this.optionsEdit.setText(LocaleManager.translate(Strings.SERVER_ACTION_EDIT));
this.optionsRefresh.setText(LocaleManager.translate(Strings.SERVER_ACTION_REFRESH));
this.optionsSessions.setText(LocaleManager.translate(Strings.SERVER_ACTION_SESSIONS));
this.optionsDelete.setText(LocaleManager.translate(Strings.SERVER_ACTION_DELETE));
}
@Override
protected void updateItem(Server server, boolean empty) {
super.updateItem(server, empty);
this.root.setVisible(server != null || !empty);
this.hBox.setVisible(server != null || !empty);
if (empty) {
return;
}
if (server == null) {
return;
}
if (this.server != server) {
resetCell();
}
server.setCell(this);
this.server = server;
setName(server.getName());
Image favicon = GUITools.getImage(server.getFavicon());
if (favicon == null) {
favicon = GUITools.MINOSOFT_LOGO;
}
this.faviconField.setImage(favicon);
if (server.isConnected()) {
this.root.getStyleClass().add("list-cell-connected");
this.optionsSessions.setDisable(false);
} else {
this.optionsSessions.setDisable(true);
}
if (server.getLastPing() == null) {
server.ping();
}
server.getLastPing().registerEvent(new CallbackEventInvoker<ServerListStatusArriveEvent>(event -> Platform.runLater(() -> {
ServerListPing ping = event.getServerListPing();
if (server != this.server) {
// cell does not contains us anymore
return;
}
resetCell();
if (server.isConnected()) {
this.root.getStyleClass().add("list-cell-connected");
}
if (ping == null) {
// Offline
this.playersField.setText("");
this.versionField.setText(LocaleManager.translate(Strings.OFFLINE));
this.versionField.getStyleClass().add("version-error");
setErrorMotd(String.format("%s", server.getLastPing().getLastException()));
this.optionsConnect.setDisable(true);
this.canConnect = false;
return;
}
this.playersField.setText(LocaleManager.translate(Strings.SERVER_INFO_SLOTS_PLAYERS_ONLINE, ping.getPlayerOnline(), ping.getMaxPlayers()));
Version serverVersion;
if (server.getDesiredVersionId() == ProtocolDefinition.QUERY_PROTOCOL_VERSION_ID) {
serverVersion = Versions.getVersionByProtocolId(ping.getProtocolId());
} else {
serverVersion = Versions.getVersionById(server.getDesiredVersionId());
this.versionField.setStyle("-fx-text-fill: -secondary-light-light-color;");
}
if (serverVersion == null) {
this.versionField.setText(ping.getServerBrand());
this.versionField.setStyle("-fx-text-fill: red;");
this.optionsConnect.setDisable(true);
this.canConnect = false;
} else {
this.versionField.setText(serverVersion.getVersionName());
this.optionsConnect.setDisable(false);
this.canConnect = true;
}
this.brandField.setText(ping.getServerModInfo().getBrand());
this.brandField.setTooltip(new Tooltip(ping.getServerModInfo().getInfo()));
this.motdField.getChildren().setAll(ping.getMotd().getJavaFXText());
if (ping.getFavicon() != null) {
this.faviconField.setImage(GUITools.getImage(ping.getFavicon()));
if (!Arrays.equals(ping.getFavicon(), server.getFavicon())) {
server.setFavicon(ping.getFavicon());
server.saveToConfig();
}
}
if (server.isReadOnly()) {
this.optionsEdit.setDisable(true);
this.optionsDelete.setDisable(true);
}
if (server.getLastPing().getLastException() != null) {
// connection failed because of an error in minosoft, but ping was okay
this.versionField.setStyle("-fx-text-fill: red;");
this.optionsConnect.setDisable(true);
this.canConnect = false;
setErrorMotd(String.format("%s: %s", server.getLastPing().getLastException().getClass().getCanonicalName(), server.getLastPing().getLastException().getMessage()));
}
})));
server.getLastPing().registerEvent(new CallbackEventInvoker<ServerListPongEvent>(event -> Platform.runLater(() -> {
this.pingField.setText(String.format("%dms", event.getLatency()));
switch (PingBars.byPing(event.getLatency())) {
case BARS_5 -> this.pingField.getStyleClass().add("ping-5-bars");
case BARS_4 -> this.pingField.getStyleClass().add("ping-4-bars");
case BARS_3 -> this.pingField.getStyleClass().add("ping-3-bars");
case BARS_2 -> this.pingField.getStyleClass().add("ping-2-bars");
case BARS_1 -> this.pingField.getStyleClass().add("ping-1-bars");
case NO_CONNECTION -> this.pingField.getStyleClass().add("ping-no-connection");
}
})));
}
public void setName(ChatComponent name) {
this.nameField.getChildren().setAll(name.getJavaFXText());
for (Node node : this.nameField.getChildren()) {
node.setStyle("-fx-font-size: 15pt ;");
}
}
private void resetCell() {
// clear all cells
setStyle(null);
this.root.getStyleClass().removeAll("list-cell-connected");
this.root.getStyleClass().removeAll("list-cell-connecting");
this.motdField.getChildren().clear();
this.brandField.setText("");
this.brandField.setTooltip(null);
this.motdField.setStyle(null);
this.versionField.setText(LocaleManager.translate(Strings.CONNECTING));
this.versionField.getStyleClass().removeAll("version-error");
this.versionField.setStyle(null);
this.playersField.setText("");
this.pingField.setText("");
this.pingField.getStyleClass().removeIf((s -> s.startsWith("ping")));
this.optionsConnect.setDisable(true);
this.optionsEdit.setDisable(false);
this.optionsDelete.setDisable(false);
}
private void setErrorMotd(String message) {
Text text = new Text(message);
text.setFill(Color.RED);
this.motdField.getChildren().setAll(text);
}
public void delete() {
if (this.server.isReadOnly()) {
return;
}
this.server.getConnections().forEach(PlayConnection::disconnect);
this.server.delete();
Log.info(String.format("Deleted server (name=\"%s\", address=\"%s\")", this.server.getName().getLegacyText(), this.server.getAddress()));
SERVER_LIST_VIEW.getItems().remove(this.server);
}
public void refresh() {
if (this.server.getLastPing() == null) {
// server was not pinged, don't even try, only costs memory and cpu
return;
}
Log.info(String.format("Refreshing server status (serverName=\"%s\", address=\"%s\")", this.server.getName().getLegacyText(), this.server.getAddress()));
resetCell();
this.server.ping();
}
public void clicked(MouseEvent e) {
switch (e.getButton()) {
case PRIMARY -> {
if (e.getClickCount() == 2) {
connect();
}
}
case SECONDARY -> this.optionsMenu.fire();
case MIDDLE -> {
SERVER_LIST_VIEW.getSelectionModel().select(this.server);
editServer();
}
}
}
public void connect() {
if (!this.canConnect || this.server.getLastPing() == null) {
return;
}
if (this.server.isConnected()) {
return;
}
this.root.getStyleClass().add("list-cell-connecting");
Minosoft.THREAD_POOL.execute(() -> {
Version version;
if (this.server.getDesiredVersionId() == ProtocolDefinition.QUERY_PROTOCOL_VERSION_ID) {
version = this.server.getLastPing().getServerVersion();
} else {
version = Versions.getVersionById(this.server.getDesiredVersionId());
}
PlayConnection connection = new PlayConnection(this.server.getLastPing().getRealAddress(), Minosoft.getConfig().getConfig().getAccount().getEntries().get(Minosoft.getConfig().getConfig().getAccount().getSelected()), version);
this.server.addConnection(connection);
Platform.runLater(() -> {
this.optionsConnect.setDisable(true);
});
// ToDo: show progress dialog
connection.registerEvent(new CallbackEventInvoker<>(this::handleConnectionCallback));
connection.connect(new CountUpAndDownLatch(1));
});
}
public void editServer() {
MainWindow.addOrEditServer(this.server);
}
private void handleConnectionCallback(ConnectionStateChangeEvent event) {
PlayConnection connection = (PlayConnection) event.getConnection();
if (!this.server.getConnections().contains(connection)) {
// the card got recycled
return;
}
Platform.runLater(() -> {
this.root.getStyleClass().removeAll("list-cell-connecting");
this.root.getStyleClass().removeAll("list-cell-connected");
this.root.getStyleClass().removeAll("list-cell-disconnecting");
this.root.getStyleClass().removeAll("list-cell-failed");
this.root.getStyleClass().add(switch (connection.getConnectionState()) {
case CONNECTING, HANDSHAKING, LOGIN -> "list-cell-connecting";
case PLAY -> "list-cell-connected";
case DISCONNECTING -> "list-cell-disconnecting";
case FAILED, FAILED_NO_RETRY -> "list-cell-failed";
default -> "";
});
if (connection.isConnected()) {
this.optionsConnect.setDisable(Minosoft.getConfig().getConfig().getAccount().getSelected().equals(connection.getAccount().getId()));
this.optionsSessions.setDisable(false);
return;
}
if (this.server.isConnected()) {
this.optionsSessions.setDisable(false);
this.optionsConnect.setDisable(false);
return;
}
this.optionsConnect.setDisable(false);
this.optionsSessions.setDisable(true);
});
}
public void showInfo() {
JFXAlert<?> dialog = new JFXAlert<>();
dialog.setTitle("View server info: " + this.server.getName().getMessage());
GUITools.initializePane(dialog.getDialogPane());
JFXDialogLayout layout = new JFXDialogLayout();
JFXButton closeButton = new JFXButton(ButtonType.CLOSE.getText());
closeButton.setOnAction((actionEvent -> dialog.hide()));
closeButton.setButtonType(JFXButton.ButtonType.RAISED);
layout.setActions(closeButton);
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(10);
TextFlow serverNameLabel = new TextFlow();
serverNameLabel.getChildren().setAll(this.server.getName().getJavaFXText());
Label serverAddressLabel = new Label(this.server.getAddress());
Label forcedVersionLabel = new Label();
if (this.server.getDesiredVersionId() == ProtocolDefinition.QUERY_PROTOCOL_VERSION_ID) {
forcedVersionLabel.setText(Versions.AUTOMATIC_VERSION.getVersionName());
} else {
forcedVersionLabel.setText(Versions.getVersionById(this.server.getDesiredVersionId()).getVersionName());
}
int column = -1;
Label a = new Label(LocaleManager.translate(Strings.SERVER_NAME) + ":");
a.setWrapText(false);
grid.add(a, 0, ++column);
grid.add(serverNameLabel, 1, column);
grid.add(new Label(LocaleManager.translate(Strings.SERVER_ADDRESS) + ":"), 0, ++column);
grid.add(serverAddressLabel, 1, column);
grid.add(new Label(LocaleManager.translate(Strings.FORCED_VERSION) + ":"), 0, ++column);
grid.add(forcedVersionLabel, 1, column);
if (this.server.getLastPing() != null) {
if (this.server.getLastPing().getLastException() != null) {
Label lastConnectionExceptionLabel = new Label(this.server.getLastPing().getLastException().toString());
lastConnectionExceptionLabel.setStyle("-fx-text-fill: red");
grid.add(new Label(LocaleManager.translate(Strings.SERVER_INFO_LAST_CONNECTION_EXCEPTION) + ":"), 0, ++column);
grid.add(lastConnectionExceptionLabel, 1, column);
}
if (this.server.getLastPing().getLastPing() != null) {
ServerListPing lastPing = this.server.getLastPing().getLastPing();
Version serverVersion = Versions.getVersionByProtocolId(lastPing.getProtocolId());
String serverVersionString;
if (serverVersion == null) {
serverVersionString = LocaleManager.translate(Strings.SERVER_INFO_VERSION_UNKNOWN, lastPing.getProtocolId());
} else {
serverVersionString = serverVersion.getVersionName();
}
Label realServerAddressLabel = new Label(this.server.getLastPing().getAddress());
Label serverVersionLabel = new Label(serverVersionString);
Label serverBrandLabel = new Label(lastPing.getServerBrand());
Label playersOnlineMaxLabel = new Label(LocaleManager.translate(Strings.SERVER_INFO_SLOTS_PLAYERS_ONLINE, lastPing.getPlayerOnline(), lastPing.getMaxPlayers()));
TextFlow motdLabel = new TextFlow();
motdLabel.getChildren().setAll(lastPing.getMotd().getJavaFXText());
Label moddedBrandLabel = new Label(lastPing.getServerModInfo().getBrand());
grid.add(new Label(LocaleManager.translate(Strings.SERVER_INFO_REAL_SERVER_ADDRESS) + ":"), 0, ++column);
grid.add(realServerAddressLabel, 1, column);
grid.add(new Label(LocaleManager.translate(Strings.VERSION) + ":"), 0, ++column);
grid.add(serverVersionLabel, 1, column);
grid.add(new Label(LocaleManager.translate(Strings.SERVER_INFO_SERVER_BRAND) + ":"), 0, ++column);
grid.add(serverBrandLabel, 1, column);
grid.add(new Label(LocaleManager.translate(Strings.SERVER_INFO_PLAYERS_ONLINE) + ":"), 0, ++column);
grid.add(playersOnlineMaxLabel, 1, column);
grid.add(new Label(LocaleManager.translate(Strings.SERVER_INFO_MESSAGE_OF_THE_DAY) + ":"), 0, ++column);
grid.add(motdLabel, 1, column);
grid.add(new Label(LocaleManager.translate(Strings.SERVER_INFO_SERVER_MODDED_BRAND) + ":"), 0, ++column);
grid.add(moddedBrandLabel, 1, column);
if (lastPing.getServerModInfo() instanceof ForgeModInfo forgeModInfo) {
Label moddedModsLabel = new Label(forgeModInfo.getModList().toString());
moddedModsLabel.setWrapText(true);
grid.add(new Label(LocaleManager.translate(Strings.SERVER_INFO_SERVER_MODDED_MOD_LIST) + ":"), 0, ++column);
grid.add(moddedModsLabel, 1, column);
}
}
}
// ToDo: size probably
layout.setBody(grid);
dialog.setContent(layout);
dialog.showAndWait();
}
public void manageSessions() {
try {
SessionsWindow sessionsWindow = GUITools.showPane(new ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "layout/sessions.fxml"), Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.SESSIONS_DIALOG_TITLE, this.server.getName().getMessage()));
sessionsWindow.setServer(this.server);
} catch (IOException e) {
e.printStackTrace();
}
}
}

View File

@ -0,0 +1,358 @@
/*
* Minosoft
* Copyright (C) 2020 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.main
import de.bixilon.minosoft.Minosoft
import de.bixilon.minosoft.data.locale.LocaleManager
import de.bixilon.minosoft.data.locale.Strings
import de.bixilon.minosoft.data.mappings.ResourceLocation
import de.bixilon.minosoft.data.mappings.versions.Version
import de.bixilon.minosoft.data.mappings.versions.Versions
import de.bixilon.minosoft.data.player.tab.PingBars
import de.bixilon.minosoft.data.text.ChatComponent
import de.bixilon.minosoft.modding.event.CallbackEventInvoker
import de.bixilon.minosoft.modding.event.events.ConnectionStateChangeEvent
import de.bixilon.minosoft.modding.event.events.ServerListPongEvent
import de.bixilon.minosoft.modding.event.events.ServerListStatusArriveEvent
import de.bixilon.minosoft.protocol.network.connection.PlayConnection
import de.bixilon.minosoft.protocol.ping.ServerModInfo
import de.bixilon.minosoft.protocol.protocol.ConnectionStates
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import de.bixilon.minosoft.util.CountUpAndDownLatch
import de.bixilon.minosoft.util.logging.Log
import javafx.application.Platform
import javafx.fxml.FXMLLoader
import javafx.fxml.Initializable
import javafx.scene.control.*
import javafx.scene.image.ImageView
import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent
import javafx.scene.layout.GridPane
import javafx.scene.layout.HBox
import javafx.scene.paint.Color
import javafx.scene.text.Text
import javafx.scene.text.TextFlow
import javafx.stage.Modality
import java.net.URL
import java.util.*
class ServerListCell : ListCell<Server?>(), Initializable {
lateinit var hBox: HBox
lateinit var root: GridPane
lateinit var faviconField: ImageView
lateinit var nameField: TextFlow
lateinit var motdField: TextFlow
lateinit var versionField: Label
lateinit var playersField: Label
lateinit var brandField: Label
lateinit var optionsConnect: MenuItem
lateinit var optionsShowInfo: MenuItem
lateinit var optionsEdit: MenuItem
lateinit var optionsRefresh: MenuItem
lateinit var optionsSessions: MenuItem
lateinit var optionsDelete: MenuItem
lateinit var optionsMenu: MenuButton
lateinit var pingField: Label
private var server: Server? = null
private var connectable = true
private var _name: ChatComponent = ChatComponent.valueOf(raw = "")
var name: ChatComponent
get() = _name
set(value) {
_name = value
nameField.children.setAll(name.javaFXText)
for (node in nameField.children) {
node.style = "-fx-font-size: 15pt ;"
}
}
override fun initialize(url: URL, rb: ResourceBundle?) {
updateSelected(false)
graphic = this.root
// change locale
optionsConnect.text = LocaleManager.translate(Strings.SERVER_ACTION_CONNECT)
optionsShowInfo.text = LocaleManager.translate(Strings.SERVER_ACTION_SHOW_INFO)
optionsEdit.text = LocaleManager.translate(Strings.SERVER_ACTION_EDIT)
optionsRefresh.text = LocaleManager.translate(Strings.SERVER_ACTION_REFRESH)
optionsSessions.text = LocaleManager.translate(Strings.SERVER_ACTION_SESSIONS)
optionsDelete.text = LocaleManager.translate(Strings.SERVER_ACTION_DELETE)
}
override fun updateItem(server: Server?, empty: Boolean) {
super.updateItem(server, empty)
this.root.isVisible = server != null || !empty
this.hBox.isVisible = server != null || !empty
if (empty || server == null) {
return
}
resetCell()
server.cell = this
this.server = server
name = server.name
faviconField.image = GUITools.getImage(server.favicon) ?: GUITools.MINOSOFT_LOGO
if (server.isConnected) {
root.styleClass.add("list-cell-connected")
optionsSessions.isDisable = false
} else {
optionsSessions.isDisable = true
}
if (server.lastPing == null) {
server.ping()
}
server.lastPing.registerEvent(CallbackEventInvoker.of<ServerListStatusArriveEvent> {
Platform.runLater {
val ping = it.serverListPing
if (this.server != server) {
// this cell does not belong here anymore
return@runLater
}
resetCell()
if (server.isConnected) {
root.styleClass.add("list-cell-connected")
}
if (ping == null) {
this.playersField.text = ""
this.versionField.text = LocaleManager.translate(Strings.OFFLINE)
this.versionField.styleClass.add("version-error")
setErrorMotd(server.lastPing.lastException.toString())
this.optionsConnect.isDisable = true
this.connectable = false
return@runLater
}
this.playersField.text = LocaleManager.translate(Strings.SERVER_INFO_SLOTS_PLAYERS_ONLINE, ping.playerOnline, ping.maxPlayers)
val serverVersion: Version
if (server.desiredVersionId == ProtocolDefinition.QUERY_PROTOCOL_VERSION_ID) {
serverVersion = Versions.getVersionByProtocolId(ping.protocolId)
} else {
serverVersion = Versions.getVersionById(server.desiredVersionId)
versionField.style = "-fx-text-fill: -secondary-light-light-color;"
}
if (serverVersion == null) {
versionField.text = ping.serverBrand
versionField.style = "-fx-text-fill: red;"
optionsConnect.isDisable = true
connectable = false
} else {
versionField.text = serverVersion.versionName
optionsConnect.isDisable = false
connectable = true
}
ping.getServerModInfo<ServerModInfo>().let { modInfo ->
brandField.text = modInfo.brand
brandField.tooltip = Tooltip(modInfo.info)
}
motdField.children.setAll(ping.motd.javaFXText)
ping.favicon?.let {
this.faviconField.image = GUITools.getImage(ping.favicon)
if (!Arrays.equals(ping.favicon, server.favicon)) {
server.favicon = ping.favicon
server.saveToConfig()
}
}
if (server.isTemporary) {
this.optionsEdit.isDisable = true
this.optionsDelete.isDisable = true
}
server.lastPing.lastException?.let { exception ->
// connection failed because of an error in minosoft, but ping was okay
this.versionField.style = "-fx-text-fill: red;"
this.optionsConnect.isDisable = true
this.connectable = false
setErrorMotd(String.format("%s: %s", exception::class.java.canonicalName, exception.message))
}
server.lastPing.registerEvent(CallbackEventInvoker.of<ServerListPongEvent> {
Platform.runLater {
this.pingField.text = "${it.latency}ms"
when (PingBars.byPing(it.latency)) {
PingBars.BARS_5 -> this.pingField.styleClass.add("ping-5-bars")
PingBars.BARS_4 -> this.pingField.styleClass.add("ping-4-bars")
PingBars.BARS_3 -> this.pingField.styleClass.add("ping-3-bars")
PingBars.BARS_2 -> this.pingField.styleClass.add("ping-2-bars")
PingBars.BARS_1 -> this.pingField.styleClass.add("ping-1-bars")
PingBars.NO_CONNECTION -> this.pingField.styleClass.add("ping-no-connection")
}
}
})
}
})
}
private fun resetCell() {
// clear all cells
style = null
this.root.styleClass.removeAll("list-cell-connected")
this.root.styleClass.removeAll("list-cell-connecting")
motdField.children.clear()
brandField.text = ""
brandField.tooltip = null
motdField.style = null
versionField.text = LocaleManager.translate(Strings.CONNECTING)
versionField.styleClass.removeAll("version-error")
versionField.style = null
playersField.text = ""
pingField.text = ""
pingField.styleClass.removeIf { s: String -> s.startsWith("ping") }
optionsConnect.isDisable = true
optionsEdit.isDisable = false
optionsDelete.isDisable = false
}
private fun setErrorMotd(message: String) {
val text = Text(message)
text.fill = Color.RED
motdField.children.setAll(text)
}
fun connect() {
val server = this.server!!
if (!this.connectable || server.lastPing == null) {
return
}
if (server.isConnected) {
return
}
this.root.styleClass.add("list-cell-connecting")
Minosoft.THREAD_POOL.execute {
val version: Version = if (server.desiredVersionId == ProtocolDefinition.QUERY_PROTOCOL_VERSION_ID) {
server.lastPing.serverVersion!!
} else {
Versions.getVersionById(server.desiredVersionId)
}
val connection = PlayConnection(server.lastPing.realAddress, Minosoft.getConfig().config.account.entries[Minosoft.getConfig().config.account.selected]!!, version)
server.addConnection(connection)
Platform.runLater { optionsConnect.isDisable = true }
// ToDo: show progress dialog
connection.registerEvent(CallbackEventInvoker.of<ConnectionStateChangeEvent> {
if (!server.connections.contains(connection)) {
// the card got recycled
return@of
}
Platform.runLater {
this.root.styleClass.removeAll("list-cell-connecting")
this.root.styleClass.removeAll("list-cell-connected")
this.root.styleClass.removeAll("list-cell-disconnecting")
this.root.styleClass.removeAll("list-cell-failed")
this.root.styleClass.add(when (connection.connectionState) {
ConnectionStates.CONNECTING, ConnectionStates.HANDSHAKING, ConnectionStates.LOGIN -> "list-cell-connecting"
ConnectionStates.PLAY -> "list-cell-connected"
ConnectionStates.DISCONNECTING -> "list-cell-disconnecting"
ConnectionStates.FAILED, ConnectionStates.FAILED_NO_RETRY -> "list-cell-failed"
else -> ""
})
if (connection.isConnected) {
optionsConnect.isDisable = Minosoft.getConfig().config.account.selected == connection.account.id
optionsSessions.isDisable = false
return@runLater
}
if (server.isConnected) {
optionsSessions.isDisable = false
optionsConnect.isDisable = false
return@runLater
}
optionsConnect.isDisable = false
optionsSessions.isDisable = true
}
})
connection.connect(CountUpAndDownLatch(1))
}
}
fun showInfo() {
TODO("Not implemented yet!")
}
fun clicked(event: MouseEvent) {
when (event.button) {
MouseButton.PRIMARY -> {
if (event.clickCount == 2) {
connect()
}
}
MouseButton.SECONDARY -> this.optionsMenu.fire()
MouseButton.MIDDLE -> {
SERVER_LIST_VIEW.selectionModel.select(this.server)
editServer()
}
else -> {
}
}
}
fun delete() {
val server = this.server!!
if (server.isTemporary) {
return
}
for (connection in server.connections) {
connection.disconnect()
}
server.delete()
Log.info("Deleted server (name=\"${server.name}\", address=\"${server.address}\")")
SERVER_LIST_VIEW.items.remove(server)
}
fun refresh() {
val server = this.server!!
if (server.lastPing == null) {
// server was not pinged, don't even try, only costs memory and cpu
return
}
Log.info("Refreshing server status (serverName=\"${server.name}\", address=\"${server.address}\")")
resetCell()
server.ping()
}
fun editServer() {
MainWindow.addOrEditServer(server)
}
fun manageSessions() {
val sessionsWindow = GUITools.showPane<SessionsWindow>(ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "layout/sessions.fxml"), Modality.APPLICATION_MODAL, LocaleManager.translate(Strings.SESSIONS_DIALOG_TITLE, server!!.name.message))
sessionsWindow.setServer(server)
}
companion object {
@JvmField
val SERVER_LIST_VIEW = ListView<Server>()
@JvmStatic
fun newInstance(): ServerListCell {
val loader = FXMLLoader(Minosoft.MINOSOFT_ASSETS_MANAGER.getAssetURL(ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "layout/cells/server.fxml")))
loader.load<Any>()
return loader.getController()
}
}
}

View File

@ -1,115 +0,0 @@
/*
* Minosoft
* Copyright (C) 2020 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.main;
import de.bixilon.minosoft.Minosoft;
import de.bixilon.minosoft.data.locale.LocaleManager;
import de.bixilon.minosoft.data.locale.Strings;
import de.bixilon.minosoft.data.mappings.ResourceLocation;
import de.bixilon.minosoft.modding.event.CallbackEventInvoker;
import de.bixilon.minosoft.modding.event.events.ConnectionStateChangeEvent;
import de.bixilon.minosoft.protocol.network.connection.PlayConnection;
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition;
import javafx.application.Platform;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
public class SessionListCell extends ListCell<PlayConnection> implements Initializable {
public static final ListView<PlayConnection> CONNECTION_LIST_VIEW = new ListView<>();
public Label account;
public Label connectionId;
public MenuItem optionsDisconnect;
public AnchorPane root;
private PlayConnection connection;
public static SessionListCell newInstance() {
FXMLLoader loader = new FXMLLoader(Minosoft.MINOSOFT_ASSETS_MANAGER.getAssetURL(new ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "layout/cells/session.fxml")));
try {
loader.load();
return loader.getController();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
@Override
public void initialize(URL url, ResourceBundle rb) {
updateSelected(false);
setGraphic(this.root);
this.optionsDisconnect.setText(LocaleManager.translate(Strings.SESSIONS_ACTION_DISCONNECT));
}
public AnchorPane getRoot() {
return this.root;
}
@Override
protected void updateItem(PlayConnection connection, boolean empty) {
super.updateItem(connection, empty);
this.root.setVisible(!empty);
if (empty) {
return;
}
if (connection == null) {
return;
}
if (connection.equals(this.connection)) {
return;
}
setStyle(null);
this.connection = connection;
connection.registerEvent(new CallbackEventInvoker<>(this::handleConnectionCallback));
this.connectionId.setText(String.format("#%d", connection.getConnectionId()));
this.account.setText(connection.getAccount().getUsername());
}
private void handleConnectionCallback(ConnectionStateChangeEvent event) {
PlayConnection connection = (PlayConnection) event.getConnection();
if (this.connection != connection) {
// the card got recycled
return;
}
if (!connection.isConnected()) {
Platform.runLater(() -> {
CONNECTION_LIST_VIEW.getItems().remove(connection);
if (CONNECTION_LIST_VIEW.getItems().isEmpty()) {
((Stage) this.root.getScene().getWindow()).close();
}
});
}
}
public void disconnect() {
setStyle("-fx-background-color: indianred");
this.connection.disconnect();
}
}

View File

@ -0,0 +1,101 @@
/*
* Minosoft
* Copyright (C) 2020 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.main
import de.bixilon.minosoft.Minosoft
import de.bixilon.minosoft.data.locale.LocaleManager
import de.bixilon.minosoft.data.locale.Strings
import de.bixilon.minosoft.data.mappings.ResourceLocation
import de.bixilon.minosoft.modding.event.CallbackEventInvoker
import de.bixilon.minosoft.modding.event.events.ConnectionStateChangeEvent
import de.bixilon.minosoft.protocol.network.connection.PlayConnection
import de.bixilon.minosoft.protocol.protocol.ProtocolDefinition
import javafx.application.Platform
import javafx.fxml.FXMLLoader
import javafx.fxml.Initializable
import javafx.scene.control.Label
import javafx.scene.control.ListCell
import javafx.scene.control.ListView
import javafx.scene.control.MenuItem
import javafx.scene.layout.AnchorPane
import javafx.stage.Stage
import java.net.URL
import java.util.*
class SessionListCell : ListCell<PlayConnection?>(), Initializable {
lateinit var account: Label
lateinit var connectionId: Label
lateinit var optionsDisconnect: MenuItem
lateinit var root: AnchorPane
private var connection: PlayConnection? = null
override fun initialize(url: URL, rb: ResourceBundle?) {
updateSelected(false)
graphic = this.root
optionsDisconnect.text = LocaleManager.translate(Strings.SESSIONS_ACTION_DISCONNECT)
}
override fun updateItem(connection: PlayConnection?, empty: Boolean) {
super.updateItem(connection, empty)
this.root.isVisible = !empty
if (empty) {
return
}
if (connection == null) {
return
}
if (connection == this.connection) {
return
}
style = null
this.connection = connection
connection.registerEvent(CallbackEventInvoker.of<ConnectionStateChangeEvent> {
handleConnectionCallback(it)
})
connectionId.text = String.format("#%d", connection.connectionId)
account.text = connection.account.username
}
private fun handleConnectionCallback(event: ConnectionStateChangeEvent) {
val connection = event.connection as PlayConnection
if (this.connection != connection) {
// the card got recycled
return
}
if (!connection.isConnected) {
Platform.runLater {
CONNECTION_LIST_VIEW.items.remove(connection)
if (CONNECTION_LIST_VIEW.items.isEmpty()) {
(this.root.scene.window as Stage).close()
}
}
}
}
fun disconnect() {
style = "-fx-background-color: indianred"
connection!!.disconnect()
}
companion object {
@JvmField
val CONNECTION_LIST_VIEW = ListView<PlayConnection>()
@JvmStatic
fun newInstance(): SessionListCell? {
val loader = FXMLLoader(Minosoft.MINOSOFT_ASSETS_MANAGER.getAssetURL(ResourceLocation(ProtocolDefinition.MINOSOFT_NAMESPACE, "layout/cells/session.fxml")))
loader.load<Any>()
return loader.getController()
}
}
}

View File

@ -129,17 +129,17 @@ class RenderWindow(
}
init {
connection.registerEvent(CallbackEventInvoker<ConnectionStateChangeEvent> {
connection.registerEvent(CallbackEventInvoker.of<ConnectionStateChangeEvent> {
if (it.connection.isDisconnected) {
renderQueue.add {
glfwSetWindowShouldClose(windowId, true)
}
}
})
connection.registerEvent(CallbackEventInvoker<PacketReceiveEvent> {
connection.registerEvent(CallbackEventInvoker.of<PacketReceiveEvent> {
val packet = it.packet
if (packet !is PacketPlayerPositionAndRotation) {
return@CallbackEventInvoker
return@of
}
if (latch.count > 0) {
latch.countDown()

View File

@ -127,19 +127,19 @@ class WorldRenderer(
prepareWorld(world)
}
connection.registerEvent(CallbackEventInvoker<ChunkUnloadEvent> {
connection.registerEvent(CallbackEventInvoker.of<ChunkUnloadEvent> {
unloadChunk(it.chunkPosition)
})
connection.registerEvent(CallbackEventInvoker<ChunkDataChangeEvent> {
connection.registerEvent(CallbackEventInvoker.of<ChunkDataChangeEvent> {
prepareChunk(it.chunkPosition)
})
connection.registerEvent(CallbackEventInvoker<BlockChangeEvent> {
connection.registerEvent(CallbackEventInvoker.of<BlockChangeEvent> {
prepareChunkSection(it.blockPosition.chunkPosition, it.blockPosition.sectionHeight)
})
connection.registerEvent(CallbackEventInvoker<MultiBlockChangeEvent> {
connection.registerEvent(CallbackEventInvoker.of<MultiBlockChangeEvent> {
val sectionHeights: MutableSet<Int> = mutableSetOf()
for ((key) in it.blocks) {
sectionHeights.add(key.sectionHeight)

View File

@ -1,47 +0,0 @@
/*
* Minosoft
* Copyright (C) 2020 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.modding.event;
import de.bixilon.minosoft.modding.event.events.Event;
import de.bixilon.minosoft.modding.loading.Priorities;
public class CallbackEventInvoker<V extends Event> extends EventInvoker {
private final InvokerCallback<V> callback;
public CallbackEventInvoker(boolean ignoreCancelled, InvokerCallback<V> callback) {
super(ignoreCancelled, Priorities.NORMAL, null);
this.callback = callback;
}
// if you need instant fireing support
public CallbackEventInvoker(InvokerCallback<V> callback) {
this(false, callback);
}
@SuppressWarnings("unchecked")
public void invoke(Event event) {
try {
this.callback.handle((V) event);
} catch (ClassCastException ignored) {
}
}
public Class<? extends Event> getEventType() {
return Event.class;
}
public interface InvokerCallback<V> {
void handle(V event);
}
}

View File

@ -0,0 +1,43 @@
/*
* Minosoft
* Copyright (C) 2020 Moritz Zwerger
*
* This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.modding.event
import de.bixilon.minosoft.modding.event.events.Event
import de.bixilon.minosoft.modding.loading.Priorities
class CallbackEventInvoker<E : Event?> private constructor(
ignoreCancelled: Boolean,
private val callback: (event: E) -> Unit,
private val eventType: Class<out Event>,
) : EventInvoker(ignoreCancelled, Priorities.NORMAL, null) {
override fun invoke(event: Event) {
callback.invoke(event as E)
}
override fun getEventType(): Class<out Event> {
return eventType
}
companion object {
@JvmOverloads
@Suppress("NON_PUBLIC_CALL_FROM_PUBLIC_INLINE")
inline fun <reified E : Event> of(ignoreCancelled: Boolean = false, noinline callback: (event: E) -> Unit): CallbackEventInvoker<E> {
return CallbackEventInvoker(
ignoreCancelled = ignoreCancelled,
callback = callback,
eventType = E::class.java
)
}
}
}

View File

@ -56,6 +56,9 @@ abstract class Connection {
}
for (eventInvoker in eventListeners) {
if (!eventInvoker.eventType.isAssignableFrom(connectionEvent::class.java)) {
continue
}
eventInvoker.invoke(connectionEvent)
}
if (connectionEvent is CancelableEvent) {

View File

@ -149,10 +149,11 @@ class StatusConnection(
}
override fun registerEvent(method: EventInvoker) {
if (method.eventType.isAssignableFrom(ServerListStatusArriveEvent::class.java) && wasPingDone()) {
val wasPingDone = wasPingDone()
if (method.eventType.isAssignableFrom(ServerListStatusArriveEvent::class.java) && wasPingDone) {
// ping done
method.invoke(ServerListStatusArriveEvent(this, this.lastPing))
} else if (method.eventType.isAssignableFrom(ServerListPongEvent::class.java) && wasPingDone() && this.pong != null) {
} else if (method.eventType.isAssignableFrom(ServerListPongEvent::class.java) && wasPingDone && this.pong != null) {
method.invoke(this.pong)
} else {
super.registerEvent(method)