package xyz.brysonsteck.serverfordummies.controllers import kotlinx.coroutines.* import kotlinx.coroutines.javafx.JavaFx import org.rauschig.jarchivelib.* import java.io.File import java.io.IOException import java.io.BufferedReader import java.io.InputStreamReader import java.awt.Checkbox import java.awt.Desktop import java.util.Properties import java.net.URL import java.net.URI import javafx.beans.value.ChangeListener import javafx.beans.value.ObservableValue import javafx.concurrent.Task import javafx.beans.property.BooleanProperty import javafx.collections.FXCollections import javafx.fxml.FXML import javafx.fxml.FXMLLoader import javafx.geometry.Insets import javafx.scene.control.Button import javafx.scene.control.ChoiceBox import javafx.scene.control.Label import javafx.scene.control.TextField import javafx.scene.control.Spinner import javafx.scene.control.TitledPane import javafx.scene.control.ButtonBar import javafx.scene.control.CheckBox import javafx.scene.control.ProgressBar import javafx.scene.control.Hyperlink import javafx.scene.layout.Border import javafx.scene.layout.BorderStroke import javafx.scene.layout.GridPane import javafx.scene.layout.Pane import javafx.scene.layout.HBox import javafx.scene.layout.VBox import javafx.scene.text.TextAlignment import javafx.scene.text.Text import javafx.scene.Scene import javafx.scene.input.MouseEvent import javafx.scene.image.Image import javafx.scene.image.ImageView import javafx.stage.FileChooser import javafx.stage.FileChooser.ExtensionFilter import javafx.stage.DirectoryChooser import javafx.stage.Modality import javafx.stage.Stage import javafx.event.EventHandler import org.rauschig.jarchivelib.* import Download import xyz.brysonsteck.serverfordummies.server.Server import xyz.brysonsteck.serverfordummies.App class PrimaryController { @FXML lateinit private var currentDirectoryLabel: Label @FXML lateinit private var worldNameField: TextField @FXML lateinit private var seedField: TextField @FXML lateinit private var portSpinner: Spinner @FXML lateinit private var difficultyBox: ChoiceBox @FXML lateinit private var gamemodeBox: ChoiceBox @FXML lateinit private var worldTypeBox: ChoiceBox @FXML lateinit private var worldSettingsPane: HBox @FXML lateinit private var parentPane: Pane @FXML lateinit private var directoryPane: Pane @FXML lateinit private var buttonBar: ButtonBar @FXML lateinit private var flightCheckbox: CheckBox @FXML lateinit private var netherCheckbox: CheckBox @FXML lateinit private var structuresCheckbox: CheckBox @FXML lateinit private var pvpCheckbox: CheckBox @FXML lateinit private var whitelistCheckbox: CheckBox @FXML lateinit private var cmdBlocksCheckbox: CheckBox @FXML lateinit private var playerCountCheckbox: CheckBox @FXML lateinit private var maxPlayersSpinner: Spinner @FXML lateinit private var maxSizeSpinner: Spinner @FXML lateinit private var memorySpinner: Spinner @FXML lateinit private var spawnSpinner: Spinner @FXML lateinit private var simulationSpinner: Spinner @FXML lateinit private var renderSpinner: Spinner @FXML lateinit private var maxTickSpinner: Spinner @FXML lateinit private var statusBar: Label @FXML lateinit private var progressBar: ProgressBar @FXML lateinit private var startButton: Button @FXML lateinit private var buildButton: Button @FXML lateinit private var defaultsButton: Button lateinit private var server: Server private var building = false private var directory = "" private var asyncResult = false private var started = false @FXML public fun initialize() { difficultyBox.items = FXCollections.observableArrayList( "Peaceful", "Easy", "Normal", "Hard", "Hardcore" ) difficultyBox.value = "Normal" difficultyBox.selectionModel.selectedIndexProperty().addListener { _, _, new -> onChoiceBoxChange("difficulty", difficultyBox.items[new as Int]) } gamemodeBox.items = FXCollections.observableArrayList( "Survival", "Creative", "Adventure", "Spectator" ) gamemodeBox.value = "Survival" gamemodeBox.selectionModel.selectedIndexProperty().addListener { _, _, new -> onChoiceBoxChange("gamemode", gamemodeBox.items[new as Int]) } worldTypeBox.items = FXCollections.observableArrayList( "Normal", "Superflat", "Large Biomes", "Amplified" ) worldTypeBox.value = "Normal" worldTypeBox.selectionModel.selectedIndexProperty().addListener { _, _, new -> onChoiceBoxChange("world-type", worldTypeBox.items[new as Int]) } } @FXML private fun onDirectoryButtonClick() { val dirChooser = DirectoryChooser() dirChooser.title = "Open a server directory" dirChooser.initialDirectory = File(System.getProperty("user.home")) val result = dirChooser.showDialog(null) if (result != null) { currentDirectoryLabel.text = result.absolutePath server = Server() val res = loadServerDir(result.absolutePath) if (res) { parentPane.isDisable = false worldSettingsPane.isDisable = false buildButton.isDisable = false defaultsButton.isDisable = false } else { currentDirectoryLabel.text = "" parentPane.isDisable = true worldSettingsPane.isDisable = true startButton.isDisable = true buildButton.isDisable = true defaultsButton.isDisable = true } } } @FXML private fun onWorldNameChange() { } @FXML private fun onSeedChange() { } @FXML private fun onPortChange() { } @FXML private fun onCheckboxClick() { } @FXML private fun onSpinnerChange() { } private fun onChoiceBoxChange(box: String, selection: String) { } @FXML private fun onInfo() { val stage = Stage() val scene = Scene(FXMLLoader(App().javaClass.getResource("info.fxml")).load(), 398.0, 358.0) stage.icons.add(Image(App().javaClass.getResourceAsStream("app-256x256.png"))) stage.setResizable(false) stage.initModality(Modality.APPLICATION_MODAL); stage.title = "About ServerCraft" stage.scene = scene stage.show() } @FXML private fun onBuild() { if (building) { building = false return; } building = true worldSettingsPane.isDisable = true directoryPane.isDisable = true parentPane.isDisable = true startButton.isDisable = true defaultsButton.isDisable = true buildButton.text = "Cancel Build" @Suppress("OPT_IN_USAGE") GlobalScope.launch(Dispatchers.Default) { progressBar.isVisible = true var javaFile = "" var archiver = ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP) val os = System.getProperty("os.name").lowercase() when { os.contains("win") -> { javaFile = "openjdk-20.0.1_windows-x64_bin.zip" archiver = ArchiverFactory.createArchiver(ArchiveFormat.ZIP) } os.contains("linux") -> { javaFile = "openjdk-20.0.1_linux-x64_bin.tar.gz" } os.contains("mac") -> { javaFile = "openjdk-20.0.1_macos-x64_bin.tar.gz" } } // download files val downloads = mapOf( "Java 20" to "https://download.java.net/java/GA/jdk20.0.1/b4887098932d415489976708ad6d1a4b/9/GPL/${javaFile}", "BuildTools" to "https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar", ) val destinations = mapOf ( "Java 20" to directory + "ServerForDummies" + File.separator + "Java" + File.separator, "BuildTools" to directory + "ServerForDummies" + File.separator + "Spigot" + File.separator ) val spigotBuilt = File(destinations["BuildTools"]).exists() val javaExtracted = File(destinations["Java 20"] + "jdk-20.0.1").exists() destinations.forEach { File(it.value).mkdir() } downloads.forEach { if (it.key == "Java 20" && javaExtracted) { return@forEach } withContext(Dispatchers.JavaFx){ statusBar.text = "Downloading ${it.key}..." progressBar.progress = ProgressBar.INDETERMINATE_PROGRESS } 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("") if (prog >= 0.01) { withContext(Dispatchers.JavaFx) {progressBar.progress = prog} } if (!building) download.status = Download.Status.CANCELLED Thread.sleep(300) } } // extract java archive if (building && !javaExtracted) { withContext(Dispatchers.JavaFx) { progressBar.progress = ProgressBar.INDETERMINATE_PROGRESS statusBar.text = "Extracting Java archive..." } var stream = archiver.stream(File(directory + "ServerForDummies" + File.separator + "Java" + File.separator + javaFile)) val dest = File(directory + "ServerForDummies" + File.separator + "Java") var entries = 0.0 while(stream.getNextEntry() != null && building) { entries++ } stream = archiver.stream(File(directory + "ServerForDummies" + File.separator + "Java" + File.separator + javaFile)) var entry = stream.getNextEntry() var currentEntry = 0.0 do { withContext(Dispatchers.JavaFx) {progressBar.progress = currentEntry/entries} entry.extract(dest) entry = stream.getNextEntry() currentEntry++ } while (entry != null && building) } if (building) { withContext(Dispatchers.JavaFx) { progressBar.progress = ProgressBar.INDETERMINATE_PROGRESS statusBar.text = "Building Minecraft Server..." } val builder = ProcessBuilder("java", "-jar", "BuildTools.jar", "--rev", "latest", "-o", ".." + File.separator + ".." + File.separator) builder.directory(File(directory + "ServerForDummies" + File.separator + "Spigot")) val proc = builder.start() val reader = InputStreamReader(proc.inputStream) val br = BufferedReader(reader) try { var line = br.readLine() var currentline = 0.0 while (line != null) { if (!building) { proc.destroy() } println(line) line = br.readLine() currentline++ if (currentline > 15) { withContext(Dispatchers.JavaFx) {progressBar.progress = if (spigotBuilt) {currentline/1100.0} else {currentline/14122.0} } } } } catch (e: IOException) { println("Stream closed") } } progressBar.isVisible = false withContext(Dispatchers.JavaFx){ worldSettingsPane.isDisable = false directoryPane.isDisable = false parentPane.isDisable = false defaultsButton.isDisable = false buildButton.text = "Build Server" statusBar.text = if (building) { findServerJar() buildButton.text = "Rebuild Server" startButton.isDisable = false "Ready." } else { "Server build cancelled." } building = false; } } } @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) return; } if (!File(directory + "eula.txt").exists()) { val res = eulaDialog() if (res) { File(directory + "eula.txt").writeText("eula=true") } else { return; } } started = true statusBar.text = "The Minecraft Server is now running. Shutdown the server to unlock the settings." worldSettingsPane.isDisable = true directoryPane.isDisable = true parentPane.isDisable = true buildButton.isDisable = true defaultsButton.isDisable = true startButton.text = "Kill Server" @Suppress("OPT_IN_USAGE") GlobalScope.launch(Dispatchers.Default) { val builder = ProcessBuilder("java", "-jar", "${server.jar}") builder.directory(File(directory)) val proc = builder.start() val reader = InputStreamReader(proc.inputStream) val br = BufferedReader(reader) try { var line = br.readLine() while (line != null) { if (asyncResult) { proc.destroy() } println(line); line = br.readLine() } } catch (e: IOException) { println("Stream closed") } withContext(Dispatchers.JavaFx) { statusBar.text = if (asyncResult) { asyncResult = false "Server killed." } else { "Server stopped." } worldSettingsPane.isDisable = false directoryPane.isDisable = false parentPane.isDisable = false buildButton.isDisable = false defaultsButton.isDisable = false startButton.text = "Start Server" started = false } } } private fun eulaDialog(): Boolean { var result = false val resources = this.javaClass.getResource("icons/warning.png") val dialog = Stage() dialog.setResizable(false) dialog.initModality(Modality.APPLICATION_MODAL); dialog.title = directory val scenePane = Pane() val dialogScene = Scene(scenePane, 400.0, 150.0); val imagePane = Pane() val icon = Image("$resources") imagePane.layoutX = 14.0 imagePane.layoutY = 14.0 imagePane.scaleX = 0.7 imagePane.scaleY = 0.7 imagePane.children.add(ImageView(icon)) val label = Label("Do you agree to the terms of the Minecraft End User License Agreement?") label.isWrapText = true label.layoutX = 115.0 label.layoutY = 40.0 val buttonBar = ButtonBar() buttonBar.buttonOrder = "L+R" buttonBar.padding = Insets(10.0, 10.0, 10.0, 10.0) buttonBar.layoutX = 0.0 buttonBar.layoutY = 107.0 buttonBar.prefWidth = 400.0 val noButton = Button("I Disagree") noButton.onMouseClicked = EventHandler() { result = false dialog.hide() } noButton.isDefaultButton = true val yesButton = Button("I Agree") yesButton.onMouseClicked = EventHandler() { result = true dialog.hide() } val eula = Button("View EULA") eula.onMouseClicked = EventHandler() { val desktop = Desktop.getDesktop() if (desktop.isSupported(Desktop.Action.BROWSE)) { try { desktop.browse(URI("https://account.mojang.com/documents/minecraft_eula")) } catch (e: Exception) { println(e) } } } ButtonBar.setButtonData(eula, ButtonBar.ButtonData.LEFT) ButtonBar.setButtonData(noButton, ButtonBar.ButtonData.RIGHT) ButtonBar.setButtonData(yesButton, ButtonBar.ButtonData.RIGHT) buttonBar.buttons.add(eula) buttonBar.buttons.add(noButton) buttonBar.buttons.add(yesButton) scenePane.children.addAll(imagePane, label, buttonBar) dialog.setScene(dialogScene); dialog.showAndWait(); return result } private fun createDialog(type: String, msg: String, yes: String, no: String, hold: Boolean): Boolean { var result = false val resources = this.javaClass.getResource("icons/$type.png") val dialog = Stage() dialog.setResizable(false) dialog.initModality(Modality.APPLICATION_MODAL); dialog.title = directory val scenePane = Pane() val dialogScene = Scene(scenePane, 400.0, 150.0); val imagePane = Pane() val icon = Image("$resources") imagePane.layoutX = 14.0 imagePane.layoutY = 14.0 imagePane.scaleX = 0.7 imagePane.scaleY = 0.7 imagePane.children.add(ImageView(icon)) val label = Label(msg) label.isWrapText = true label.layoutX = 115.0 label.layoutY = if (type == "warning") {10.0} else {40.0} val buttonBar = ButtonBar() buttonBar.padding = Insets(10.0, 10.0, 10.0, 10.0) buttonBar.layoutX = 0.0 buttonBar.layoutY = 107.0 buttonBar.prefWidth = 400.0 val noButton = Button(no) noButton.onMouseClicked = EventHandler() { if (hold) { result = false } else { asyncResult = false } dialog.hide() } val yesButton = Button(yes) yesButton.onMouseClicked = EventHandler() { if (hold) { result = true } else { asyncResult = true } dialog.hide() } yesButton.isDefaultButton = true buttonBar.buttons.add(noButton) buttonBar.buttons.add(yesButton) scenePane.children.addAll(imagePane, label, buttonBar) dialog.setScene(dialogScene); if (hold) { dialog.showAndWait(); } else { dialog.show(); } return result } private fun loadServerDir(dir: String): Boolean { directory = dir if (!File(directory).isDirectory) { return false; } if (directory[directory.length-1] != File.separatorChar) directory += File.separatorChar val hasDummy = File(directory + "ServerForDummies").isDirectory val hasProperties = File(directory + File.separator + "server.properties").isFile val hasServer = findServerJar() if (hasDummy && hasServer) { // server complete, just read jproperties statusBar.text = "Server found!" startButton.isDisable = false buildButton.text = "Rebuild Server" } else if (hasDummy && !hasServer && hasProperties) { // just needs to be built startButton.isDisable = true 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 \nServerForDummies. Errors may occur; copying\nthe world directories to a new folder may be\nsafer. Proceed anyway?", "Yes", "No", true) statusBar.text = "Ready." if (result) { startButton.isDisable = false } return result } else { // assume clean directory val result = createDialog("info", "There is no server in this directory.\nCreate one?", "Yes", "No", true) if (result) { File(directory + "ServerForDummies").mkdir() startButton.isDisable = true buildButton.text = "Build Server" } statusBar.text = "Ready." return result } return true; } private fun findServerJar(): Boolean { // search for spigot jar // major version for (i in 25 downTo 8) { // patch number for (j in 15 downTo 0) { var spigotFile: String = "" if (j == 0) spigotFile += "spigot-1.$i.jar" else spigotFile += "spigot-1.$i.$j.jar"; if (File(directory + spigotFile).isFile) { server.jar = directory + spigotFile return true } } } // try vanilla server if no spigot server if (File(directory + "server.jar").isFile) { server.jar = directory + "server.jar" return true } return false } }