Launcher: manage active connections to a server (=sessions)

This commit is contained in:
Bixilon 2020-09-06 16:28:41 +02:00
parent f7dd739aad
commit 744189c745
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
12 changed files with 338 additions and 17 deletions

View File

@ -30,7 +30,7 @@ import java.util.UUID;
import static de.bixilon.minosoft.protocol.protocol.ProtocolDefinition.PLAYER_INVENTORY_ID; import static de.bixilon.minosoft.protocol.protocol.ProtocolDefinition.PLAYER_INVENTORY_ID;
public class Player { public class Player {
final MojangAccount acc; final MojangAccount account;
final ScoreboardManager scoreboardManager = new ScoreboardManager(); final ScoreboardManager scoreboardManager = new ScoreboardManager();
public final HashMap<UUID, PlayerListItem> playerList = new HashMap<>(); public final HashMap<UUID, PlayerListItem> playerList = new HashMap<>();
float health; float health;
@ -49,22 +49,22 @@ public class Player {
TextComponent tabHeader; TextComponent tabHeader;
TextComponent tabFooter; TextComponent tabFooter;
public Player(MojangAccount acc) { public Player(MojangAccount account) {
this.acc = acc; this.account = account;
// create our own inventory without any properties // create our own inventory without any properties
inventories.put(PLAYER_INVENTORY_ID, new Inventory(null)); inventories.put(PLAYER_INVENTORY_ID, new Inventory(null));
} }
public String getPlayerName() { public String getPlayerName() {
return acc.getPlayerName(); return account.getPlayerName();
} }
public UUID getPlayerUUID() { public UUID getPlayerUUID() {
return acc.getUUID(); return account.getUUID();
} }
public MojangAccount getAccount() { public MojangAccount getAccount() {
return this.acc; return this.account;
} }
public float getHealth() { public float getHealth() {

View File

@ -112,6 +112,7 @@ public class AccountListCell extends ListCell<MojangAccount> implements Initiali
public void select() { public void select() {
Minosoft.selectAccount(account); Minosoft.selectAccount(account);
listView.refresh(); listView.refresh();
ServerListCell.listView.refresh();
} }
private void resetCell() { private void resetCell() {

View File

@ -0,0 +1,20 @@
/*
* Codename 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.protocol.network.Connection;
public interface ConnectionChangeCallback {
void handle(Connection connection);
}

View File

@ -20,6 +20,7 @@ import de.bixilon.minosoft.protocol.protocol.ConnectionReasons;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.util.ArrayList;
public class Server { public class Server {
static int highestServerId; static int highestServerId;
@ -29,6 +30,7 @@ public class Server {
int desiredVersion; int desiredVersion;
String favicon; String favicon;
Connection lastPing; Connection lastPing;
ArrayList<Connection> connections = new ArrayList<>();
public Server(int id, String name, String address, int desiredVersion) { public Server(int id, String name, String address, int desiredVersion) {
this.id = id; this.id = id;
@ -121,4 +123,21 @@ public class Server {
} }
lastPing.resolve(ConnectionReasons.PING, getDesiredVersion()); // resolve dns address and ping lastPing.resolve(ConnectionReasons.PING, getDesiredVersion()); // resolve dns address and ping
} }
public ArrayList<Connection> getConnections() {
return connections;
}
public void addConnection(Connection connection) {
connections.add(connection);
}
public boolean isConnected() {
for (Connection connection : connections) {
if (connection.isConnected()) {
return true;
}
}
return false;
}
} }

View File

@ -28,6 +28,8 @@ import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.geometry.Insets; import javafx.geometry.Insets;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
@ -35,6 +37,8 @@ import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane; import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.GridPane; import javafx.scene.layout.GridPane;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.util.Pair; import javafx.util.Pair;
import java.io.IOException; import java.io.IOException;
@ -59,6 +63,8 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
public MenuButton optionsMenu; public MenuButton optionsMenu;
@FXML @FXML
public Label serverBrand; public Label serverBrand;
@FXML
public MenuItem optionsSessions;
boolean canConnect = false; boolean canConnect = false;
@FXML @FXML
private Label serverName; private Label serverName;
@ -114,6 +120,12 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
favicon = GUITools.logo; favicon = GUITools.logo;
} }
icon.setImage(favicon); icon.setImage(favicon);
if (server.isConnected()) {
setStyle("-fx-background-color: darkseagreen;");
optionsSessions.setDisable(false);
} else {
optionsSessions.setDisable(true);
}
if (server.getLastPing() == null) { if (server.getLastPing() == null) {
server.ping(); server.ping();
} }
@ -123,6 +135,10 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
return; return;
} }
resetCell(); resetCell();
if (server.isConnected()) {
setStyle("-fx-background-color: darkseagreen;");
}
if (ping == null) { if (ping == null) {
// Offline // Offline
players.setText(""); players.setText("");
@ -257,10 +273,14 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
} else { } else {
version = Versions.getVersionById(server.getDesiredVersion()); version = Versions.getVersionById(server.getDesiredVersion());
} }
optionsConnect.setDisable(true);
connection.connect(server.getLastPing().getAddress(), version); connection.connect(server.getLastPing().getAddress(), version);
setStyle("-fx-background-color: darkseagreen;"); connection.addConnectionChangeCallback(this::handleConnectionCallback);
server.addConnection(connection);
} }
private void resetCell() { private void resetCell() {
// clear all cells // clear all cells
setStyle(null); setStyle(null);
@ -384,4 +404,43 @@ public class ServerListCell extends ListCell<Server> implements Initializable {
dialog.showAndWait(); dialog.showAndWait();
} }
public void manageSessions() {
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/layout/sessions.fxml"));
Parent parent = loader.load();
((SessionsWindow) loader.getController()).setServer(server);
Stage stage = new Stage();
stage.initModality(Modality.APPLICATION_MODAL);
stage.setTitle(String.format("Sessions - %s - Minosoft", server.getName()));
stage.setScene(new Scene(parent));
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
private void handleConnectionCallback(Connection connection) {
if (!server.getConnections().contains(connection)) {
// the card got recycled
return;
}
Platform.runLater(() -> {
if (!connection.isConnected()) {
// maybe we got disconnected
if (!server.isConnected()) {
setStyle(null);
optionsSessions.setDisable(true);
optionsConnect.setDisable(false);
}
return;
}
if (Minosoft.getSelectedAccount() != connection.getPlayer().getAccount()) {
optionsConnect.setDisable(false);
}
setStyle("-fx-background-color: darkseagreen;");
optionsSessions.setDisable(false);
});
}
} }

View File

@ -0,0 +1,111 @@
/*
* Codename 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.protocol.network.Connection;
import javafx.application.Platform;
import javafx.fxml.FXML;
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.layout.AnchorPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.net.URL;
import java.util.ResourceBundle;
public class SessionListCell extends ListCell<Connection> implements Initializable {
public static ListView<Connection> listView = new ListView<>();
@FXML
public Label account;
@FXML
public Label connectionId;
@FXML
private AnchorPane root;
private Connection connection;
public static SessionListCell newInstance() {
FXMLLoader loader = new FXMLLoader(SessionListCell.class.getResource("/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(root);
}
public AnchorPane getRoot() {
return root;
}
@Override
protected void updateItem(Connection connection, boolean empty) {
super.updateItem(connection, empty);
root.setVisible(!empty);
if (empty) {
return;
}
if (connection == null) {
return;
}
if (connection.equals(this.connection)) {
return;
}
setStyle(null);
this.connection = connection;
connection.addConnectionChangeCallback(this::handleConnectionCallback);
connectionId.setText(String.format("#%d", connection.getConnectionId()));
account.setText(connection.getPlayer().getAccount().getPlayerName());
}
@Override
public void updateSelected(boolean selected) {
super.updateSelected(selected);
}
public void disconnect() {
setStyle("-fx-background-color: indianred");
connection.disconnect();
}
private void handleConnectionCallback(Connection connection) {
if (this.connection != connection) {
// the card got recycled
return;
}
if (!connection.isConnected()) {
Platform.runLater(() -> {
listView.getItems().remove(connection);
if (listView.getItems().size() == 0) {
((Stage) root.getScene().getWindow()).close();
}
});
}
}
}

View File

@ -0,0 +1,52 @@
/*
* Codename 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.protocol.network.Connection;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.BorderPane;
import java.net.URL;
import java.util.ResourceBundle;
public class SessionsWindow implements Initializable {
@FXML
public BorderPane accountPane;
Server server;
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
SessionListCell.listView.setCellFactory((lv) -> SessionListCell.newInstance());
}
public void setServer(Server server) {
this.server = server;
ObservableList<Connection> connections = FXCollections.observableArrayList();
for (Connection connection : server.getConnections()) {
if (!connection.isConnected()) {
server.getConnections().remove(connection);
}
connections.add(connection);
}
SessionListCell.listView.setItems(connections);
accountPane.setCenter(SessionListCell.listView);
}
public void disconnectAll() {
server.getConnections().forEach(Connection::disconnect);
}
}

View File

@ -22,6 +22,7 @@ import de.bixilon.minosoft.game.datatypes.objectLoader.CustomMapping;
import de.bixilon.minosoft.game.datatypes.objectLoader.recipes.Recipes; import de.bixilon.minosoft.game.datatypes.objectLoader.recipes.Recipes;
import de.bixilon.minosoft.game.datatypes.objectLoader.versions.Version; import de.bixilon.minosoft.game.datatypes.objectLoader.versions.Version;
import de.bixilon.minosoft.game.datatypes.objectLoader.versions.Versions; import de.bixilon.minosoft.game.datatypes.objectLoader.versions.Versions;
import de.bixilon.minosoft.gui.main.ConnectionChangeCallback;
import de.bixilon.minosoft.logging.Log; import de.bixilon.minosoft.logging.Log;
import de.bixilon.minosoft.logging.LogLevels; import de.bixilon.minosoft.logging.LogLevels;
import de.bixilon.minosoft.ping.ServerListPing; import de.bixilon.minosoft.ping.ServerListPing;
@ -52,6 +53,7 @@ public class Connection {
final LinkedBlockingQueue<ClientboundPacket> handlingQueue = new LinkedBlockingQueue<>(); final LinkedBlockingQueue<ClientboundPacket> handlingQueue = new LinkedBlockingQueue<>();
final VelocityHandler velocityHandler = new VelocityHandler(this); final VelocityHandler velocityHandler = new VelocityHandler(this);
final HashSet<PingCallback> pingCallbacks = new HashSet<>(); final HashSet<PingCallback> pingCallbacks = new HashSet<>();
final HashSet<ConnectionChangeCallback> connectionChangeCallbacks = new HashSet<>();
final int connectionId; final int connectionId;
final Player player; final Player player;
final String hostname; final String hostname;
@ -197,13 +199,15 @@ public class Connection {
resolve(address); resolve(address);
} else { } else {
// no connection and no servers available anymore... sorry, but you can not play today :( // no connection and no servers available anymore... sorry, but you can not play today :(
handleCallbacks(null); handlePingCallbacks(null);
} }
break; break;
case FAILED_NO_RETRY: case FAILED_NO_RETRY:
handleCallbacks(null); handlePingCallbacks(null);
break; break;
} }
// handle callbacks
connectionChangeCallbacks.forEach((callback -> callback.handle(this)));
} }
public Version getVersion() { public Version getVersion() {
@ -361,13 +365,13 @@ public class Connection {
return connectionId; return connectionId;
} }
public void addPingCallback(PingCallback pingCallback) { public void addPingCallback(PingCallback callback) {
if (getConnectionState() == ConnectionStates.FAILED || getConnectionState() == ConnectionStates.FAILED_NO_RETRY || lastPing != null) { if (getConnectionState() == ConnectionStates.FAILED || getConnectionState() == ConnectionStates.FAILED_NO_RETRY || lastPing != null) {
// ping done // ping done
pingCallback.handle(lastPing); callback.handle(lastPing);
return; return;
} }
pingCallbacks.add(pingCallback); pingCallbacks.add(callback);
} }
public HashSet<PingCallback> getPingCallbacks() { public HashSet<PingCallback> getPingCallbacks() {
@ -382,17 +386,23 @@ public class Connection {
this.desiredVersionNumber = desiredVersionNumber; this.desiredVersionNumber = desiredVersionNumber;
} }
public void handleCallbacks(ServerListPing ping) { public void handlePingCallbacks(ServerListPing ping) {
this.lastPing = ping; this.lastPing = ping;
for (PingCallback callback : getPingCallbacks()) { pingCallbacks.forEach((callback -> callback.handle(ping)));
callback.handle(ping);
}
} }
public Exception getLastConnectionException() { public Exception getLastConnectionException() {
return network.lastException; return network.lastException;
} }
public void addConnectionChangeCallback(ConnectionChangeCallback callback) {
connectionChangeCallbacks.add(callback);
}
public HashSet<ConnectionChangeCallback> getConnectionChangeCallbacks() {
return connectionChangeCallbacks;
}
public ServerListPing getLastPing() { public ServerListPing getLastPing() {
return lastPing; return lastPing;
} }

View File

@ -68,7 +68,7 @@ public class PacketHandler {
connection.setVersion(version); connection.setVersion(version);
} }
Log.info(String.format("Status response received: %s/%s online. MotD: '%s'", pkg.getResponse().getPlayerOnline(), pkg.getResponse().getMaxPlayers(), pkg.getResponse().getMotd().getColoredMessage())); Log.info(String.format("Status response received: %s/%s online. MotD: '%s'", pkg.getResponse().getPlayerOnline(), pkg.getResponse().getMaxPlayers(), pkg.getResponse().getMotd().getColoredMessage()));
connection.handleCallbacks(pkg.getResponse()); connection.handlePingCallbacks(pkg.getResponse());
} }
public void handle(PacketStatusPong pkg) { public void handle(PacketStatusPong pkg) {

View File

@ -31,6 +31,8 @@
<MenuItem mnemonicParsing="false" onAction="#showInfo" text="Info"/> <MenuItem mnemonicParsing="false" onAction="#showInfo" text="Info"/>
<MenuItem mnemonicParsing="false" onAction="#edit" text="Edit"/> <MenuItem mnemonicParsing="false" onAction="#edit" text="Edit"/>
<MenuItem mnemonicParsing="false" onAction="#refresh" text="Refresh"/> <MenuItem mnemonicParsing="false" onAction="#refresh" text="Refresh"/>
<MenuItem fx:id="optionsSessions" mnemonicParsing="false" onAction="#manageSessions" disable="true"
text="Sessions"/>
<MenuItem mnemonicParsing="false" style="-fx-text-fill: red" onAction="#delete" text="Delete"/> <MenuItem mnemonicParsing="false" style="-fx-text-fill: red" onAction="#delete" text="Delete"/>
</items> </items>
</MenuButton> </MenuButton>

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuButton?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.Font?>
<AnchorPane xmlns:fx="http://javafx.com/fxml/1" fx:id="root" maxHeight="-Infinity" maxWidth="500.0"
minHeight="-Infinity" minWidth="500.0" prefWidth="500.0" xmlns="http://javafx.com/javafx/11.0.1"
fx:controller="de.bixilon.minosoft.gui.main.SessionListCell">
<Label fx:id="connectionId" layoutX="111.0" layoutY="14.0" maxWidth="200.0" minWidth="10.0" text="#1"
underline="true" AnchorPane.bottomAnchor="5.0" AnchorPane.leftAnchor="5.0" AnchorPane.topAnchor="5.0">
<font>
<Font size="17.0"/>
</font>
</Label>
<Label fx:id="account" layoutX="408.0" layoutY="8.0" text="account" AnchorPane.bottomAnchor="5.0"
AnchorPane.leftAnchor="100.0" AnchorPane.topAnchor="5.0"/>
<MenuButton layoutX="389.0" layoutY="81.0" mnemonicParsing="false" text="⋮" AnchorPane.bottomAnchor="5.0"
AnchorPane.rightAnchor="5.0" AnchorPane.topAnchor="5.0">
<items>
<MenuItem mnemonicParsing="false" onAction="#disconnect" style="-fx-text-fill: red" text="Disconnect"/>
</items>
</MenuButton>
</AnchorPane>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.*?>
<VBox xmlns:fx="http://javafx.com/fxml/1" prefHeight="400.0" prefWidth="640.0" xmlns="http://javafx.com/javafx/11.0.1"
fx:controller="de.bixilon.minosoft.gui.main.SessionsWindow">
<MenuBar VBox.vgrow="NEVER">
<Menu mnemonicParsing="false" text="_Disconnect">
<MenuItem mnemonicParsing="false" onAction="#disconnectAll" text="From all"/>
</Menu>
</MenuBar>
<AnchorPane VBox.vgrow="ALWAYS">
<ScrollPane fitToHeight="true" fitToWidth="true" layoutX="14.0" layoutY="14.0" minHeight="-Infinity"
minWidth="-Infinity" prefHeight="500.0" prefWidth="500.0" AnchorPane.bottomAnchor="0.0"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<BorderPane fx:id="accountPane"/>
</ScrollPane>
</AnchorPane>
</VBox>