From 49311129e97a232074478c94db23a7285f106d99 Mon Sep 17 00:00:00 2001 From: Bryson Steck Date: Sat, 20 May 2023 20:02:08 -0600 Subject: implemented properties, console, and logging system --- build.gradle | 2 +- .../ServerCraft/controllers/PrimaryController.kt | 245 ++++++++++++++++++--- .../xyz/brysonsteck/ServerCraft/server/Download.kt | 6 +- .../xyz/brysonsteck/ServerCraft/server/Server.kt | 24 +- .../brysonsteck/ServerCraft/icons/arrow_down.png | Bin 0 -> 846 bytes .../xyz/brysonsteck/ServerCraft/icons/arrow_up.png | Bin 0 -> 841 bytes .../xyz/brysonsteck/ServerCraft/primary.fxml | 96 +++++--- 7 files changed, 298 insertions(+), 75 deletions(-) create mode 100644 src/main/resources/xyz/brysonsteck/ServerCraft/icons/arrow_down.png create mode 100644 src/main/resources/xyz/brysonsteck/ServerCraft/icons/arrow_up.png diff --git a/build.gradle b/build.gradle index 49bc3d4..3b908bf 100644 --- a/build.gradle +++ b/build.gradle @@ -89,6 +89,7 @@ task pack(type: io.github.fvarrui.javapackager.gradle.PackageTask, dependsOn: bu organizationUrl = "https://brysonsteck.xyz" organizationEmail = "me@brysonsteck.xyz" url = "https://codeberg.org/brysonsteck/ServerCraft" + additionalModules = [ "jdk.crypto.ec" ] linuxConfig { pngFile = file('src/main/resources/icon.png') @@ -113,7 +114,6 @@ task pack(type: io.github.fvarrui.javapackager.gradle.PackageTask, dependsOn: bu } } -// tasks.register('fixAppImageIcon', Copy) { build.doLast { if (OperatingSystem.current().isLinux()) { exec { diff --git a/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/PrimaryController.kt b/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/PrimaryController.kt index 99d1009..d445923 100644 --- a/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/PrimaryController.kt +++ b/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/PrimaryController.kt @@ -32,6 +32,7 @@ import javafx.scene.control.ButtonBar import javafx.scene.control.CheckBox import javafx.scene.control.ProgressBar import javafx.scene.control.Hyperlink +import javafx.scene.control.ScrollPane import javafx.scene.layout.Border import javafx.scene.layout.BorderStroke import javafx.scene.layout.GridPane @@ -50,6 +51,7 @@ import javafx.stage.DirectoryChooser import javafx.stage.Modality import javafx.stage.Stage import javafx.event.EventHandler +import javafx.event.ActionEvent import org.rauschig.jarchivelib.* import xyz.brysonsteck.ServerCraft.server.Server @@ -57,6 +59,8 @@ import xyz.brysonsteck.ServerCraft.server.Download import xyz.brysonsteck.ServerCraft.App class PrimaryController { + @FXML + lateinit private var primary: Pane @FXML lateinit private var currentDirectoryLabel: Label @FXML @@ -94,7 +98,7 @@ class PrimaryController { @FXML lateinit private var playerCountCheckbox: CheckBox @FXML - lateinit private var maxPlayersSpinner: Spinner + lateinit private var maxPlayerSpinner: Spinner @FXML lateinit private var maxSizeSpinner: Spinner @FXML @@ -117,15 +121,29 @@ class PrimaryController { lateinit private var buildButton: Button @FXML lateinit private var defaultsButton: Button + @FXML + lateinit private var dropDownIcon: ImageView + @FXML + lateinit private var console: Label + @FXML + lateinit private var scrollPane: ScrollPane lateinit private var server: Server private var building = false private var directory = "" private var asyncResult = false private var started = false + private var loading = false + private var showingConsole = false + + private fun log(str: String) { + console.text = console.text + str + "\n" + println(str) + } @FXML public fun initialize() { + scrollPane.vvalueProperty().bind(console.heightProperty()); difficultyBox.items = FXCollections.observableArrayList( "Peaceful", "Easy", @@ -135,7 +153,9 @@ class PrimaryController { ) difficultyBox.value = "Normal" difficultyBox.selectionModel.selectedIndexProperty().addListener { _, _, new -> - onChoiceBoxChange("difficulty", difficultyBox.items[new as Int]) + if (!loading) { + onPropChange("difficulty", difficultyBox.items[new as Int]) + } } gamemodeBox.items = FXCollections.observableArrayList( "Survival", @@ -145,7 +165,9 @@ class PrimaryController { ) gamemodeBox.value = "Survival" gamemodeBox.selectionModel.selectedIndexProperty().addListener { _, _, new -> - onChoiceBoxChange("gamemode", gamemodeBox.items[new as Int]) + if (!loading) { + onPropChange("gamemode", gamemodeBox.items[new as Int]) + } } worldTypeBox.items = FXCollections.observableArrayList( "Normal", @@ -155,7 +177,59 @@ class PrimaryController { ) worldTypeBox.value = "Normal" worldTypeBox.selectionModel.selectedIndexProperty().addListener { _, _, new -> - onChoiceBoxChange("world-type", worldTypeBox.items[new as Int]) + if (!loading) { + onPropChange("level-type", worldTypeBox.items[new as Int]) + } + } + maxPlayerSpinner.editor.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("max-players", new) + } + } + maxSizeSpinner.editor.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("max-world-size", new) + } + } + portSpinner.editor.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("server-port", new) + } + } + renderSpinner.editor.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("view-distance", new) + } + } + memorySpinner.editor.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("jvm-ram", new) + } + } + spawnSpinner.editor.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("spawn-protection", new) + } + } + simulationSpinner.editor.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("simulation-distance", new) + } + } + maxTickSpinner.editor.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("max-tick-time", new) + } + } + worldNameField.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("level-name", new) + } + } + seedField.textProperty().addListener { _, _, new -> + if (!loading) { + onPropChange("level-seed", new) + } } } @@ -174,6 +248,8 @@ class PrimaryController { worldSettingsPane.isDisable = false buildButton.isDisable = false defaultsButton.isDisable = false + applyProps() + server.dir = result.absolutePath } else { currentDirectoryLabel.text = "" parentPane.isDisable = true @@ -185,33 +261,114 @@ class PrimaryController { } } - @FXML - private fun onWorldNameChange() { - + private fun parseBool(bool: String): Boolean { + if (bool == "true") { + return true + } + return false } - @FXML - private fun onSeedChange() { - + private fun applyProps() { + loading = true + flightCheckbox.isSelected = parseBool(server.getProp("allow-flight")) + netherCheckbox.isSelected = parseBool(server.getProp("allow-nether")) + structuresCheckbox.isSelected = parseBool(server.getProp("generate-structures")) + pvpCheckbox.isSelected = parseBool(server.getProp("pvp")) + whitelistCheckbox.isSelected = parseBool(server.getProp("white-list")) + cmdBlocksCheckbox.isSelected = parseBool(server.getProp("enable-command-block")) + playerCountCheckbox.isSelected = parseBool(server.getProp("hide-online-players")) + maxPlayerSpinner.valueFactory.value = server.getProp("max-players").toIntOrNull() + maxSizeSpinner.valueFactory.value = server.getProp("max-world-size").toIntOrNull() + portSpinner.valueFactory.value = server.getProp("server-port").toIntOrNull() + renderSpinner.valueFactory.value = server.getProp("view-distance").toIntOrNull() + memorySpinner.valueFactory.value = server.getProp("jvm-ram").toIntOrNull() + spawnSpinner.valueFactory.value = server.getProp("spawn-protection").toIntOrNull() + simulationSpinner.valueFactory.value = server.getProp("simulation-distance").toIntOrNull() + maxTickSpinner.valueFactory.value = server.getProp("max-tick-time").toIntOrNull() + difficultyBox.value = if (parseBool(server.getProp("hardcore"))) { + "Hardcore" + } else { + server.getProp("difficulty").replaceFirstChar { it.uppercase() } + } + gamemodeBox.value = server.getProp("gamemode").replaceFirstChar { it.uppercase() } + worldTypeBox.value = server.getProp("level-type").removePrefix("minecraft:") + .split('_').joinToString(" ") { it.replaceFirstChar(Char::uppercaseChar)} + worldNameField.text = server.getProp("level-name") + seedField.text = server.getProp("level-seed") + loading = false } @FXML - private fun onPortChange() { - + private fun onCheckboxClick(e: ActionEvent) { + val box = e.target as CheckBox + when { + box == whitelistCheckbox -> { + server.setProp("white-list", whitelistCheckbox.isSelected) + } + box == pvpCheckbox -> { + server.setProp("pvp", pvpCheckbox.isSelected) + } + box == netherCheckbox -> { + server.setProp("allow-nether", netherCheckbox.isSelected) + } + box == cmdBlocksCheckbox -> { + server.setProp("enable-command-block", cmdBlocksCheckbox.isSelected) + } + box == flightCheckbox -> { + server.setProp("allow-flight", flightCheckbox.isSelected) + } + box == structuresCheckbox -> { + server.setProp("generate-structures", structuresCheckbox.isSelected) + } + box == playerCountCheckbox -> { + server.setProp("hide-online-players", playerCountCheckbox.isSelected) + } + } } - @FXML - private fun onCheckboxClick() { - + private fun onPropChange(prop: String, value: String) { + when { + prop == "gamemode" -> { + server.setProp(prop, value.lowercase()) + } + prop == "difficulty" -> { + if (value == "Hardcore") { + server.setProp("hardcode", "true") + server.setProp(prop, "hard") + } else { + server.setProp("hardcode", "false") + server.setProp(prop, value.lowercase()) + } + } + prop == "level-type" -> { + server.setProp(prop, "minecraft:" + value.lowercase().replace(" ", "_")) + } + else -> { + server.setProp(prop, value) + } + } } @FXML - private fun onSpinnerChange() { - + private fun onToggleConsole() { + if (showingConsole) { + primary.getScene().window.height = 743.0 + dropDownIcon.image = Image(App().javaClass.getResourceAsStream("icons/arrow_down.png")) + } else { + primary.getScene().window.height = 905.0 + dropDownIcon.image = Image(App().javaClass.getResourceAsStream("icons/arrow_up.png")) + } + showingConsole = !showingConsole } - private fun onChoiceBoxChange(box: String, selection: String) { - + @FXML + private fun onDefaults() { + val res = createDialog("info", "Reset settings to defaults?\nThere is NO GOING BACK!") + if (res) { + server.loadProps() + applyProps() + statusBar.text = "Resetting settings to defaults successful." + } } @FXML @@ -280,19 +437,21 @@ class PrimaryController { withContext(Dispatchers.JavaFx){ statusBar.text = "Downloading ${it.key}..." progressBar.progress = ProgressBar.INDETERMINATE_PROGRESS + log("Downloading ${it.key} from ${it.value}") } val download = Download(URL(it.value), destinations[it.key]!!) download.start() while (download.status == Download.Status.DOWNLOADING) { var prog = (download.downloaded.toDouble() / download.contentLength.toDouble()) // for whatever reason I need to print something to the screen in order for it to update the progress bar - print("") + withContext(Dispatchers.JavaFx) {log("Progress: ${prog * 100}%")} if (prog >= 0.01) { withContext(Dispatchers.JavaFx) {progressBar.progress = prog} } if (!building) download.status = Download.Status.CANCELLED Thread.sleep(300) } + withContext(Dispatchers.JavaFx) {log("Download of ${it.key} complete with status: ${download.status}")} } // extract java archive @@ -300,6 +459,7 @@ class PrimaryController { withContext(Dispatchers.JavaFx) { progressBar.progress = ProgressBar.INDETERMINATE_PROGRESS statusBar.text = "Extracting Java archive..." + log("Extracting Java archive to ${directory + "ServerCraft" + File.separator + "Java"}") } var stream = archiver.stream(File(directory + "ServerCraft" + File.separator + "Java" + File.separator + javaFile)) val dest = File(directory + "ServerCraft" + File.separator + "Java") @@ -311,7 +471,10 @@ class PrimaryController { var entry = stream.getNextEntry() var currentEntry = 0.0 do { - withContext(Dispatchers.JavaFx) {progressBar.progress = currentEntry/entries} + withContext(Dispatchers.JavaFx) { + progressBar.progress = currentEntry/entries + log(entry.name) + } entry.extract(dest) entry = stream.getNextEntry() currentEntry++ @@ -335,7 +498,7 @@ class PrimaryController { if (!building) { proc.destroy() } - println(line) + withContext(Dispatchers.JavaFx) {log(line)} line = br.readLine() currentline++ if (currentline > 15) { @@ -343,7 +506,7 @@ class PrimaryController { } } } catch (e: IOException) { - println("Stream closed") + withContext(Dispatchers.JavaFx) {log("Stream Closed")} } } @@ -370,7 +533,7 @@ class PrimaryController { @FXML private fun onStart() { if (started) { - createDialog("warning", "You should only kill the server if\nabsolutely necessary. Data loss may occur.\nContinue anyway?", "Yes", "No", false) + createDialog("warning", "You should only kill the server if\nabsolutely necessary. Data loss may occur.\nContinue anyway?", hold=false) return; } if (!File(directory + "eula.txt").exists()) { @@ -391,7 +554,7 @@ class PrimaryController { startButton.text = "Kill Server" @Suppress("OPT_IN_USAGE") GlobalScope.launch(Dispatchers.Default) { - val builder = ProcessBuilder("java", "-jar", "${server.jar}") + val builder = ProcessBuilder("java", "-Xmx${server.getProp("jvm-ram")}M", "-jar", "${server.jar}") builder.directory(File(directory)) val proc = builder.start() val reader = InputStreamReader(proc.inputStream) @@ -406,11 +569,11 @@ class PrimaryController { } proc.destroy() } - println(line); + withContext(Dispatchers.JavaFx) {log(line)} line = br.readLine() } } catch (e: IOException) { - println("Stream closed") + withContext(Dispatchers.JavaFx) {log("Stream Closed")} } withContext(Dispatchers.JavaFx) { statusBar.text = if (asyncResult) { @@ -459,18 +622,18 @@ class PrimaryController { buttonBar.layoutY = 107.0 buttonBar.prefWidth = 400.0 val noButton = Button("I Disagree") - noButton.onMouseClicked = EventHandler() { + noButton.onAction = EventHandler() { result = false dialog.hide() } noButton.isDefaultButton = true val yesButton = Button("I Agree") - yesButton.onMouseClicked = EventHandler() { + yesButton.onAction = EventHandler() { result = true dialog.hide() } val eula = Button("View EULA") - eula.onMouseClicked = EventHandler() { + eula.onAction = EventHandler() { val desktop = Desktop.getDesktop() if (desktop.isSupported(Desktop.Action.BROWSE)) { // most likely running on Windows or macOS @@ -500,7 +663,7 @@ class PrimaryController { return result } - private fun createDialog(type: String, msg: String, yes: String, no: String, hold: Boolean): Boolean { + private fun createDialog(type: String, msg: String, yes: String = "Yes", no: String = "No", hold: Boolean = true): Boolean { var result = false val resources = App().javaClass.getResource("icons/$type.png") val dialog = Stage() @@ -527,7 +690,7 @@ class PrimaryController { buttonBar.layoutY = 107.0 buttonBar.prefWidth = 400.0 val noButton = Button(no) - noButton.onMouseClicked = EventHandler() { + noButton.onAction = EventHandler() { if (hold) { result = false } else { @@ -536,7 +699,7 @@ class PrimaryController { dialog.hide() } val yesButton = Button(yes) - yesButton.onMouseClicked = EventHandler() { + yesButton.onAction = EventHandler() { if (hold) { result = true } else { @@ -559,10 +722,12 @@ class PrimaryController { private fun loadServerDir(dir: String): Boolean { directory = dir + // exit if doesn't exist for whatever reason if (!File(directory).isDirectory) { return false; } + // add system dir separator for cleaner code if (directory[directory.length-1] != File.separatorChar) directory += File.separatorChar @@ -570,7 +735,7 @@ class PrimaryController { val hasProperties = File(directory + File.separator + "server.properties").isFile val hasServer = findServerJar() - if (hasDummy && hasServer) { + if (hasDummy && hasServer && hasProperties) { // server complete, just read jproperties statusBar.text = "Server found!" startButton.isDisable = false @@ -581,24 +746,32 @@ class PrimaryController { statusBar.text = "Server needs to be built before starting." } else if (!hasDummy && hasServer) { // server created externally - val result = createDialog("warning", "This server directory was not created by \nServerCraft. Errors may occur; copying\nthe world directories to a new folder may be\nsafer. Proceed anyway?", "Yes", "No", true) + val result = createDialog("warning", "This server directory was not created by \nServerCraft. Errors may occur; copying\nthe world directories to a new folder may be\nsafer. Proceed anyway?") statusBar.text = "Ready." if (result) { startButton.isDisable = false } + server.loadProps(dir) return result } else { // assume clean directory - val result = createDialog("info", "There is no server in this directory.\nCreate one?", "Yes", "No", true) + val result = createDialog("info", "There is no server in this directory.\nCreate one?") if (result) { File(directory + "ServerCraft").mkdir() startButton.isDisable = true buildButton.text = "Build Server" } statusBar.text = "Ready." + server.loadProps() return result } + if (hasProperties) { + server.loadProps(dir) + } else { + server.loadProps() + } + return true; } diff --git a/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Download.kt b/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Download.kt index 757c32c..f538458 100644 --- a/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Download.kt +++ b/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Download.kt @@ -3,6 +3,7 @@ package xyz.brysonsteck.ServerCraft.server import java.io.*; import java.net.*; import java.util.*; +import javax.net.ssl.HttpsURLConnection class Download: Runnable { public enum class Status { @@ -43,7 +44,7 @@ class Download: Runnable { try { // Open connection to URL. - var connection = url.openConnection() as HttpURLConnection; + var connection = url.openConnection() as HttpsURLConnection; // Specify what portion of file to download. connection.setRequestProperty("Range", "bytes=" + downloaded + "-"); @@ -53,12 +54,14 @@ class Download: Runnable { // Make sure response code is in the 200 range. if (connection.responseCode / 100 != 2) { + println(connection.responseCode) status = Status.ERROR } // Check for valid content length. contentLength = connection.getContentLength(); if (contentLength < 1) { + println(connection.getContentLength()) status = Status.ERROR } @@ -99,6 +102,7 @@ class Download: Runnable { status = Status.COMPLETE; } } catch (e: Exception) { + println(e) status = Status.ERROR } finally { // Close file. diff --git a/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Server.kt b/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Server.kt index 746bc55..b591074 100644 --- a/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Server.kt +++ b/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Server.kt @@ -1,14 +1,16 @@ package xyz.brysonsteck.ServerCraft.server import java.io.File +import java.io.InputStream import java.util.Properties public class Server { public var jar = "" + public var dir = "" private val props = Properties() - constructor() { + public fun loadProps() { props.setProperty("allow-flight", false.toString()) props.setProperty("allow-nether", true.toString()) props.setProperty("generate-structures", true.toString()) @@ -32,4 +34,24 @@ public class Server { props.setProperty("level-type", "minecraft:normal") props.setProperty("motd", "A server for a dummy") } + + public fun loadProps(dir: String) { + val ins = File(dir + File.separator + "server.properties").inputStream() + props.load(ins) + } + + private fun writeProps() { + val outs = File(dir + File.separator + "server.properties").outputStream() + props.store(outs, "Minecraft server properties\nCreated with ServerCraft: https://codeberg.org/brysonsteck/ServerCraft") + } + + public fun getProp(prop: String): String { + return props.getProperty(prop) + } + + public fun setProp(key: String, value: Any) { + props.setProperty(key, value.toString()) + writeProps() + } + } \ No newline at end of file diff --git a/src/main/resources/xyz/brysonsteck/ServerCraft/icons/arrow_down.png b/src/main/resources/xyz/brysonsteck/ServerCraft/icons/arrow_down.png new file mode 100644 index 0000000..3ca5992 Binary files /dev/null and b/src/main/resources/xyz/brysonsteck/ServerCraft/icons/arrow_down.png differ diff --git a/src/main/resources/xyz/brysonsteck/ServerCraft/icons/arrow_up.png b/src/main/resources/xyz/brysonsteck/ServerCraft/icons/arrow_up.png new file mode 100644 index 0000000..a61ea1d Binary files /dev/null and b/src/main/resources/xyz/brysonsteck/ServerCraft/icons/arrow_up.png differ diff --git a/src/main/resources/xyz/brysonsteck/ServerCraft/primary.fxml b/src/main/resources/xyz/brysonsteck/ServerCraft/primary.fxml index 6b34102..cec0d2c 100644 --- a/src/main/resources/xyz/brysonsteck/ServerCraft/primary.fxml +++ b/src/main/resources/xyz/brysonsteck/ServerCraft/primary.fxml @@ -9,22 +9,25 @@ + + + - + -