eros: multiple server types (custom + LAN servers)

This commit is contained in:
Bixilon 2021-07-26 00:43:01 +02:00
parent 43d8988ef9
commit b951322066
No known key found for this signature in database
GPG Key ID: 5CAD791931B09AC4
12 changed files with 286 additions and 171 deletions

View File

@ -22,7 +22,7 @@ import java.util.*
abstract class AbstractCard<T> : ListCell<T>(), Initializable {
@FXML
protected lateinit var root: HBox
lateinit var root: HBox
override fun initialize(url: URL?, resourceBundle: ResourceBundle?) {
this.graphic = root

View File

@ -19,12 +19,12 @@ import javafx.fxml.FXMLLoader
import javafx.scene.control.ListCell
interface CardFactory<T : ListCell<*>> {
val FXML: ResourceLocation
val LAYOUT: ResourceLocation
fun build(): T {
val loader = FXMLLoader()
loader.load<Any>(Minosoft.MINOSOFT_ASSETS_MANAGER.readAssetAsStream(FXML))
loader.load<Any>(Minosoft.MINOSOFT_ASSETS_MANAGER.readAssetAsStream(LAYOUT))
return loader.getController()
}

View File

@ -13,23 +13,30 @@
package de.bixilon.minosoft.gui.eros.main.play
import de.bixilon.minosoft.Minosoft
import de.bixilon.minosoft.gui.eros.controller.EmbeddedJavaFXController
import de.bixilon.minosoft.gui.eros.main.play.server.Refreshable
import de.bixilon.minosoft.gui.eros.main.play.server.ServerListController
import de.bixilon.minosoft.gui.eros.main.play.server.type.ServerType
import de.bixilon.minosoft.gui.eros.main.play.server.type.ServerTypeCardController
import de.bixilon.minosoft.gui.eros.modding.invoker.JavaFXEventInvoker
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.text
import de.bixilon.minosoft.modding.event.events.LANServerDiscoverEvent
import de.bixilon.minosoft.protocol.protocol.LANServerListener
import de.bixilon.minosoft.util.KUtil.asResourceLocation
import javafx.fxml.FXML
import javafx.scene.control.ListView
import javafx.scene.layout.AnchorPane
import javafx.scene.layout.Pane
import org.kordamp.ikonli.fontawesome5.FontAwesomeSolid
class PlayMainController : EmbeddedJavaFXController<Pane>() {
@FXML
private lateinit var playTypeContentFX: Pane
@FXML
private lateinit var playTypeListViewFX: ListView<*>
private lateinit var playTypeListViewFX: ListView<ServerType>
@FXML
private lateinit var refreshPaneFX: AnchorPane
@ -38,12 +45,38 @@ class PlayMainController : EmbeddedJavaFXController<Pane>() {
private lateinit var currentController: EmbeddedJavaFXController<*>
override fun init() {
currentController = JavaFXUtil.loadEmbeddedController<ServerListController>(ServerListController.LAYOUT)
playTypeContentFX.children.setAll(currentController.root)
playTypeListViewFX.setCellFactory { ServerTypeCardController.build() }
playTypeListViewFX.items += ServerType(FontAwesomeSolid.SERVER, "Custom", "0 Servers", "") {
return@ServerType JavaFXUtil.loadEmbeddedController<ServerListController>(ServerListController.LAYOUT).apply {
servers = Minosoft.config.config.server.entries.values
refreshList()
}
}
playTypeListViewFX.items += ServerType(FontAwesomeSolid.NETWORK_WIRED, "LAN", "12 Servers", "") {
return@ServerType JavaFXUtil.loadEmbeddedController<ServerListController>(ServerListController.LAYOUT).apply {
readOnly = true
customRefresh = {
LANServerListener.SERVERS.clear()
refreshList()
}
servers = LANServerListener.SERVERS.values
Minosoft.GLOBAL_EVENT_MASTER.registerEvent(JavaFXEventInvoker.of<LANServerDiscoverEvent> { refreshList() }) // ToDo: Unregister event when hiding pane
refreshList()
}
}
JavaFXUtil.loadEmbeddedController<ServerTypeCardController>(ServerTypeCardController.LAYOUT).apply {
playTypeListViewFX.selectionModel.selectedItemProperty().addListener { _, _, new ->
currentController = new.content()
playTypeContentFX.children.setAll(currentController.root)
}
playTypeListViewFX.selectionModel.select(0)
ServerTypeCardController.build().apply {
refreshPaneFX.children.setAll(root)
iconFX.iconLiteral = "fas-sync-alt"
iconFX.isVisible = true
headerFX.text = REFRESH_HEADER
text1FX.text = REFRESH_TEXT1
text2FX.text = REFRESH_TEXT2

View File

@ -66,6 +66,16 @@ class ServerListController : EmbeddedJavaFXController<Pane>(), Refreshable {
@FXML
private lateinit var serverInfoFX: AnchorPane
var customRefresh: (() -> Unit)? = null
var servers: MutableCollection<Server> = mutableListOf()
var readOnly: Boolean = false
set(value) {
field = value
addServerButtonFX.isVisible = !value
}
override fun init() {
serverListViewFX.setCellFactory { ServerCardController.build() }
@ -86,7 +96,7 @@ class ServerListController : EmbeddedJavaFXController<Pane>(), Refreshable {
val selected = serverListViewFX.selectionModel.selectedItem
serverListViewFX.items.clear()
for (server in Minosoft.config.config.server.entries.values) {
for (server in servers) {
updateServer(server)
}
@ -168,43 +178,45 @@ class ServerListController : EmbeddedJavaFXController<Pane>(), Refreshable {
it.columnConstraints += ColumnConstraints()
it.columnConstraints += ColumnConstraints(0.0, -1.0, Double.POSITIVE_INFINITY, Priority.ALWAYS, HPos.LEFT, true)
it.add(Button("Delete").apply {
setOnAction {
SimpleErosConfirmationDialog(
confirmButtonText = "minosoft:general.delete".asResourceLocation(),
description = TranslatableComponents.EROS_DELETE_SERVER_CONFIRM_DESCRIPTION(Minosoft.LANGUAGE_MANAGER, serverCard.server.name, serverCard.server.address),
onConfirm = {
Minosoft.config.config.server.entries.remove(serverCard.server.id)
if (!readOnly) {
it.add(Button("Delete").apply {
setOnAction {
SimpleErosConfirmationDialog(
confirmButtonText = "minosoft:general.delete".asResourceLocation(),
description = TranslatableComponents.EROS_DELETE_SERVER_CONFIRM_DESCRIPTION(Minosoft.LANGUAGE_MANAGER, serverCard.server.name, serverCard.server.address),
onConfirm = {
Minosoft.config.config.server.entries.remove(serverCard.server.id)
Minosoft.config.saveToFile()
Platform.runLater { refreshList() }
}
).show()
}
}, 1, 0)
it.add(Button("Edit").apply {
setOnAction {
val server = serverCard.server
UpdateServerDialog(server = server, onUpdate = { name, address, forcedVersion ->
server.name = ChatComponent.of(name)
server.forcedVersion = forcedVersion
if (server.address != address) {
server.favicon = null
server.address = address
// disconnect all ping connections, re ping
// ToDo: server.connections.clear()
serverCard.unregister()
server.ping?.disconnect()
server.ping = null
server.ping()
}
Minosoft.config.saveToFile()
Platform.runLater { refreshList() }
}
).show()
}
}, 1, 0)
it.add(Button("Edit").apply {
setOnAction {
val server = serverCard.server
UpdateServerDialog(server = server, onUpdate = { name, address, forcedVersion ->
server.name = ChatComponent.of(name)
server.forcedVersion = forcedVersion
if (server.address != address) {
server.favicon = null
server.address = address
// disconnect all ping connections, re ping
// ToDo: server.connections.clear()
serverCard.unregister()
server.ping?.disconnect()
server.ping = null
server.ping()
}
Minosoft.config.saveToFile()
Platform.runLater { refreshList() }
}).show()
}
}, 2, 0)
}).show()
}
}, 2, 0)
}
it.add(Button("Refresh").apply {
setOnAction {
@ -256,15 +268,20 @@ class ServerListController : EmbeddedJavaFXController<Pane>(), Refreshable {
@FXML
fun addServer() {
UpdateServerDialog(onUpdate = { name, address, focedVersion ->
val server = Server(name = ChatComponent.of(name), address = address, forcedVersion = focedVersion)
Minosoft.config.config.server.entries[server.id] = server
UpdateServerDialog(onUpdate = { name, address, forcedVersion ->
val server = Server(name = ChatComponent.of(name), address = address, forcedVersion = forcedVersion)
Minosoft.config.config.server.entries[server.id] = server // ToDo
Minosoft.config.saveToFile()
Platform.runLater { refreshList() }
}).show()
}
override fun refresh() {
customRefresh?.let {
it()
return
}
for (serverCard in serverListViewFX.items) {
serverCard.server.ping?.let {
if (it.state != StatusConnectionStates.PING_DONE && it.state != StatusConnectionStates.ERROR) {

View File

@ -127,6 +127,6 @@ class ServerCardController : AbstractCard<ServerCard>() {
}
companion object : CardFactory<ServerCardController> {
override val FXML: ResourceLocation = "minosoft:eros/main/play/server/server_card.fxml".asResourceLocation()
override val LAYOUT: ResourceLocation = "minosoft:eros/main/play/server/server_card.fxml".asResourceLocation()
}
}

View File

@ -11,30 +11,15 @@
* This software is not affiliated with Mojang AB, the original developer of Minecraft.
*/
package de.bixilon.minosoft.gui.eros.main.play
package de.bixilon.minosoft.gui.eros.main.play.server.type
import de.bixilon.minosoft.gui.eros.controller.EmbeddedJavaFXController
import de.bixilon.minosoft.util.KUtil.asResourceLocation
import javafx.fxml.FXML
import javafx.scene.layout.HBox
import javafx.scene.text.TextFlow
import org.kordamp.ikonli.javafx.FontIcon
import org.kordamp.ikonli.fontawesome5.FontAwesomeSolid
class ServerTypeCardController : EmbeddedJavaFXController<HBox>() {
@FXML
lateinit var iconFX: FontIcon
@FXML
lateinit var headerFX: TextFlow
@FXML
lateinit var text1FX: TextFlow
@FXML
lateinit var text2FX: TextFlow
companion object {
val LAYOUT = "minosoft:eros/main/play/server_type_card.fxml".asResourceLocation()
}
}
data class ServerType(
val icon: FontAwesomeSolid,
val header: Any?,
val text1: Any?,
val text2: Any?,
val content: () -> EmbeddedJavaFXController<*>,
)

View File

@ -0,0 +1,60 @@
/*
* Minosoft
* Copyright (C) 2021 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.eros.main.play.server.type
import de.bixilon.minosoft.gui.eros.card.AbstractCard
import de.bixilon.minosoft.gui.eros.card.CardFactory
import de.bixilon.minosoft.gui.eros.util.JavaFXUtil.text
import de.bixilon.minosoft.util.KUtil.asResourceLocation
import javafx.fxml.FXML
import javafx.scene.text.TextFlow
import org.kordamp.ikonli.javafx.FontIcon
class ServerTypeCardController : AbstractCard<ServerType>() {
@FXML
lateinit var iconFX: FontIcon
@FXML
lateinit var headerFX: TextFlow
@FXML
lateinit var text1FX: TextFlow
@FXML
lateinit var text2FX: TextFlow
override fun updateItem(item: ServerType?, empty: Boolean) {
super.updateItem(item, empty)
item ?: return
iconFX.isVisible = true
iconFX.iconCode = item.icon
headerFX.text = item.header
text1FX.text = item.text1
text2FX.text = item.text2
}
override fun clear() {
iconFX.isVisible = false
headerFX.children.clear()
text1FX.children.clear()
text2FX.children.clear()
}
companion object : CardFactory<ServerTypeCardController> {
override val LAYOUT = "minosoft:eros/main/play/server_type_card.fxml".asResourceLocation()
}
}

View File

@ -68,19 +68,19 @@ object JavaFXUtil {
return controller
}
var TextFlow.text: Any
var TextFlow.text: Any?
get() = TODO()
set(value) {
this.children.setAll(Minosoft.LANGUAGE_MANAGER.translate(value).javaFXText)
}
var TextField.placeholder: Any
var TextField.placeholder: Any?
get() = this.promptText
set(value) {
this.promptText = Minosoft.LANGUAGE_MANAGER.translate(value).message
}
var Labeled.ctext: Any
var Labeled.ctext: Any?
get() = this.text
set(value) {
this.text = Minosoft.LANGUAGE_MANAGER.translate(value).message

View File

@ -0,0 +1,22 @@
/*
* Minosoft
* Copyright (C) 2021 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.events
import de.bixilon.minosoft.config.server.Server
import java.net.InetAddress
class LANServerDiscoverEvent(
val remoteAddress: InetAddress,
val serer: Server,
) : Event(), CancelableEvent

View File

@ -1,96 +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.protocol.protocol;
import com.google.common.collect.HashBiMap;
import de.bixilon.minosoft.config.server.Server;
import de.bixilon.minosoft.util.Util;
import de.bixilon.minosoft.util.logging.Log;
import de.bixilon.minosoft.util.logging.LogLevels;
import de.bixilon.minosoft.util.logging.LogMessageType;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CountDownLatch;
public class LANServerListener {
public static final HashBiMap<InetAddress, Server> SERVER_MAP = HashBiMap.create();
private static final String MOTD_BEGIN_STRING = "[MOTD]";
private static final String MOTD_END_STRING = "[/MOTD]";
private static final String PORT_START_STRING = "[AD]";
private static final String PORT_END_STRING = "[/AD]";
private static final String[] BROADCAST_MUST_CONTAIN = {MOTD_BEGIN_STRING, MOTD_END_STRING, PORT_START_STRING, PORT_END_STRING};
public static void listen() throws InterruptedException {
CountDownLatch latch = new CountDownLatch(1);
new Thread(() -> {
try {
MulticastSocket socket = new MulticastSocket(ProtocolDefinition.LAN_SERVER_BROADCAST_PORT);
socket.joinGroup(new InetSocketAddress(ProtocolDefinition.LAN_SERVER_BROADCAST_INET_ADDRESS, ProtocolDefinition.LAN_SERVER_BROADCAST_PORT), NetworkInterface.getByInetAddress(ProtocolDefinition.LAN_SERVER_BROADCAST_INET_ADDRESS));
byte[] buf = new byte[256]; // this should be enough, if the packet is longer, it is probably invalid
Log.log(LogMessageType.NETWORK_STATUS, LogLevels.INFO, () -> "Listening for LAN servers");
latch.countDown();
while (true) {
try {
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
Log.protocol(String.format("LAN UDP Broadcast from %s:%s -> %s", packet.getAddress().getHostAddress(), packet.getPort(), new String(buf)));
InetAddress sender = packet.getAddress();
if (SERVER_MAP.containsKey(sender)) {
// This guy sent us already a server, maybe just the regular 1.5 second interval, a duplicate or a DOS attack...We don't care
continue;
}
Server server = getServerByBroadcast(sender, packet.getData());
if (SERVER_MAP.containsValue(server)) {
continue;
}
if (SERVER_MAP.size() > ProtocolDefinition.LAN_SERVER_MAXIMUM_SERVERS) {
continue;
}
SERVER_MAP.put(sender, server);
Log.debug(String.format("Discovered new LAN Server: %s", server));
} catch (Exception ignored) {
}
}
} catch (Exception e) {
e.printStackTrace();
latch.countDown();
}
SERVER_MAP.clear();
Log.warn("Stopping LAN Server Listener Thread");
}, "LAN Server Listener").start();
latch.await();
}
private static Server getServerByBroadcast(InetAddress address, byte[] broadcast) {
String parsed = new String(broadcast, StandardCharsets.UTF_8); // example: [MOTD]Bixilon - New World[/MOTD][AD]41127[/AD]
for (String mustContain : BROADCAST_MUST_CONTAIN) {
if (!parsed.contains(mustContain)) {
throw new IllegalArgumentException("Broadcast is invalid!");
}
}
String rawAddress = Util.getStringBetween(parsed, PORT_START_STRING, PORT_END_STRING);
if (rawAddress.contains(":")) {
// weird, just extract the port
rawAddress = rawAddress.split(":")[1];
}
int port = Integer.parseInt(rawAddress);
if (port < 0 || port > 65535) {
throw new IllegalArgumentException(String.format("Invalid port: %d", port));
}
// return new Server(new ServerAddress(address.getHostAddress(), port));
throw new RuntimeException("TODO");
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.protocol.protocol
import com.google.common.collect.HashBiMap
import de.bixilon.minosoft.Minosoft
import de.bixilon.minosoft.config.server.Server
import de.bixilon.minosoft.modding.event.events.LANServerDiscoverEvent
import de.bixilon.minosoft.util.Util
import de.bixilon.minosoft.util.logging.Log
import de.bixilon.minosoft.util.logging.LogLevels
import de.bixilon.minosoft.util.logging.LogMessageType
import java.net.*
import java.nio.charset.StandardCharsets
import java.util.concurrent.CountDownLatch
object LANServerListener {
val SERVERS: HashBiMap<InetAddress, Server> = HashBiMap.create()
private const val MOTD_BEGIN_STRING = "[MOTD]"
private const val MOTD_END_STRING = "[/MOTD]"
private const val PORT_START_STRING = "[AD]"
private const val PORT_END_STRING = "[/AD]"
private val BROADCAST_MUST_CONTAIN = arrayOf(MOTD_BEGIN_STRING, MOTD_END_STRING, PORT_START_STRING, PORT_END_STRING)
fun listen() {
val latch = CountDownLatch(1)
Thread({
try {
val socket = MulticastSocket(ProtocolDefinition.LAN_SERVER_BROADCAST_PORT)
socket.joinGroup(InetSocketAddress(ProtocolDefinition.LAN_SERVER_BROADCAST_INET_ADDRESS, ProtocolDefinition.LAN_SERVER_BROADCAST_PORT), NetworkInterface.getByInetAddress(ProtocolDefinition.LAN_SERVER_BROADCAST_INET_ADDRESS))
val buffer = ByteArray(256) // this should be enough, if the packet is longer, it is probably invalid
Log.log(LogMessageType.NETWORK_STATUS, LogLevels.INFO) { "Listening for LAN servers..." }
latch.countDown()
while (true) {
try {
val packet = DatagramPacket(buffer, buffer.size)
socket.receive(packet)
val broadcast = String(buffer, 0, packet.length, StandardCharsets.UTF_8)
Log.log(LogMessageType.NETWORK_PACKETS_IN, LogLevels.INFO) { "Received LAN servers broadcast (${packet.address.hostAddress}:${packet.port}): $broadcast" }
val sender = packet.address
if (SERVERS.containsKey(sender)) {
// This guy sent us already a server, maybe just the regular 1.5 second interval, a duplicate or a DOS attack...We don't care
continue
}
val server = getServerByBroadcast(sender, broadcast)
if (SERVERS.containsValue(server)) {
continue
}
if (SERVERS.size > ProtocolDefinition.LAN_SERVER_MAXIMUM_SERVERS) {
continue
}
if (Minosoft.GLOBAL_EVENT_MASTER.fireEvent(LANServerDiscoverEvent(packet.address, server))) {
continue
}
SERVERS[sender] = server
Log.log(LogMessageType.NETWORK_PACKETS_IN, LogLevels.INFO) { "Discovered LAN servers: $server" }
} catch (ignored: Throwable) {
}
}
} catch (exception: Exception) {
exception.printStackTrace()
latch.countDown()
}
SERVERS.clear()
Log.log(LogMessageType.NETWORK_STATUS, LogLevels.INFO) { "Stop listening for LAN servers..." }
}, "LAN Server Listener").start()
latch.await()
}
private fun getServerByBroadcast(address: InetAddress, broadcast: String): Server {
// example: [MOTD]Bixilon - New World[/MOTD][AD]41127[/AD]
for (mustContain in BROADCAST_MUST_CONTAIN) {
require(broadcast.contains(mustContain)) { "Broadcast is invalid!" }
}
var rawAddress = Util.getStringBetween(broadcast, PORT_START_STRING, PORT_END_STRING)
if (rawAddress.contains(":")) {
// weird, just extract the port
rawAddress = rawAddress.split(":").toTypedArray()[1]
}
val port = rawAddress.toInt()
require(!(port < 0 || port > 65535)) { String.format("Invalid port: %d", port) }
return Server(address = address.hostAddress + ":" + rawAddress) // ToDo: Name
}
}

View File

@ -4,7 +4,7 @@
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<?import org.kordamp.ikonli.javafx.FontIcon?>
<HBox xmlns:fx="http://javafx.com/fxml/1" prefHeight="80.0" prefWidth="190.0" xmlns="http://javafx.com/javafx/16" fx:controller="de.bixilon.minosoft.gui.eros.main.play.ServerTypeCardController">
<HBox xmlns:fx="http://javafx.com/fxml/1" fx:id="root" prefHeight="80.0" prefWidth="190.0" xmlns="http://javafx.com/javafx/16" fx:controller="de.bixilon.minosoft.gui.eros.main.play.server.type.ServerTypeCardController">
<GridPane HBox.hgrow="ALWAYS">
<columnConstraints>
<ColumnConstraints hgrow="NEVER"/>