aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/main/kotlin/xyz/brysonsteck/ServerCraft/App.kt34
-rw-r--r--src/main/kotlin/xyz/brysonsteck/ServerCraft/Main.kt5
-rw-r--r--src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/InfoController.kt74
-rw-r--r--src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/PrimaryController.kt632
-rw-r--r--src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Download.kt119
-rw-r--r--src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Server.kt35
-rw-r--r--src/main/resources/dmg.pngbin0 -> 8844 bytes
-rw-r--r--src/main/resources/icon.icnsbin0 -> 1575945 bytes
-rw-r--r--src/main/resources/icon.icobin0 -> 220027 bytes
-rw-r--r--src/main/resources/icon.pngbin0 -> 213761 bytes
-rw-r--r--src/main/resources/xyz/brysonsteck/ServerCraft/app.pngbin0 -> 75026 bytes
-rw-r--r--src/main/resources/xyz/brysonsteck/ServerCraft/css/info-tabs.css5
-rw-r--r--src/main/resources/xyz/brysonsteck/ServerCraft/icons/info.pngbin0 -> 5289 bytes
-rw-r--r--src/main/resources/xyz/brysonsteck/ServerCraft/icons/warning.pngbin0 -> 5131 bytes
-rw-r--r--src/main/resources/xyz/brysonsteck/ServerCraft/info.fxml108
-rw-r--r--src/main/resources/xyz/brysonsteck/ServerCraft/primary.fxml362
-rw-r--r--src/test/kotlin/xyz/brysonsteck/serverfordummies/AppTest.kt14
17 files changed, 1388 insertions, 0 deletions
diff --git a/src/main/kotlin/xyz/brysonsteck/ServerCraft/App.kt b/src/main/kotlin/xyz/brysonsteck/ServerCraft/App.kt
new file mode 100644
index 0000000..5b5ddac
--- /dev/null
+++ b/src/main/kotlin/xyz/brysonsteck/ServerCraft/App.kt
@@ -0,0 +1,34 @@
+/*
+ * This Kotlin source file was generated by the Gradle 'init' task.
+ */
+package xyz.brysonsteck.ServerCraft
+
+import javafx.application.Application;
+import javafx.fxml.FXMLLoader;
+import javafx.scene.Parent;
+import javafx.scene.Scene;
+import javafx.scene.image.Image
+import javafx.stage.Stage;
+import java.awt.Desktop;
+
+class App : Application() {
+
+ override fun start(stage: Stage) {
+ var scene = Scene(loadFXML("primary"), 963.0, 713.0)
+ stage.icons.add(Image(this.javaClass.getResourceAsStream("app.png")))
+ stage.setResizable(false)
+ stage.title = "ServerCraft"
+ stage.scene = scene
+ stage.show()
+ }
+
+ public fun loadFXML(fxml: String) : Parent {
+ val fxmlLoader = FXMLLoader(this.javaClass.getResource(fxml + ".fxml"))
+ return fxmlLoader.load()
+ }
+
+ public fun run() {
+ launch()
+ }
+
+}
diff --git a/src/main/kotlin/xyz/brysonsteck/ServerCraft/Main.kt b/src/main/kotlin/xyz/brysonsteck/ServerCraft/Main.kt
new file mode 100644
index 0000000..fbb61e4
--- /dev/null
+++ b/src/main/kotlin/xyz/brysonsteck/ServerCraft/Main.kt
@@ -0,0 +1,5 @@
+package xyz.brysonsteck.ServerCraft
+
+fun main() {
+ App().run()
+} \ No newline at end of file
diff --git a/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/InfoController.kt b/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/InfoController.kt
new file mode 100644
index 0000000..dcf77ba
--- /dev/null
+++ b/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/InfoController.kt
@@ -0,0 +1,74 @@
+package xyz.brysonsteck.ServerCraft.controllers
+
+import javafx.fxml.FXML
+import javafx.application.Platform
+import javafx.scene.Node
+import javafx.scene.control.Hyperlink
+import javafx.stage.Stage
+import javafx.event.ActionEvent
+import java.awt.Desktop
+import java.net.URI
+
+class InfoController {
+ private val emails = mapOf(
+ "bryson" to "me@brysonsteck.xyz"
+ )
+ private val websites = mapOf(
+ "bryson" to "https://brysonsteck.xyz"
+ )
+ private val source = "https://codeberg.org/brysonsteck/ServerCraft"
+ private val license = "https://www.gnu.org/licenses/gpl-3.0.html"
+
+ @FXML
+ private fun openHyperlink(e: ActionEvent) {
+ val link = e.source as Hyperlink
+ link.isVisited = false
+ val split = link.id.split('_').toMutableList()
+ split.add("")
+ val os = System.getProperty("os.name").lowercase()
+
+ val desktop = Desktop.getDesktop()
+ try {
+ when {
+ split[1].equals("email") -> {
+ if (!os.contains("linux")) {
+ desktop.browse(URI("mailto:" + emails[split[0]]))
+ } else {
+ Runtime.getRuntime().exec("xdg-open mailto:" + emails[split[0]])
+ }
+ }
+ split[1].equals("website") -> {
+ if (!os.contains("linux")) {
+ desktop.browse(URI(websites[split[0]]))
+ } else {
+ Runtime.getRuntime().exec("xdg-open " + websites[split[0]])
+ }
+ }
+ split[0].equals("source") -> {
+ if (!os.contains("linux")) {
+ desktop.browse(URI(source))
+ } else {
+ Runtime.getRuntime().exec("xdg-open " + source)
+ }
+ }
+ split[0].equals("license") -> {
+ println("license")
+ if (!os.contains("linux")) {
+ desktop.browse(URI(license))
+ } else {
+ Runtime.getRuntime().exec("xdg-open " + license)
+ }
+ }
+ }
+ } catch (e: Exception) {
+ println(e)
+ }
+ }
+
+ @FXML
+ private fun closeInfo(e: ActionEvent) {
+ val source = e.getSource() as Node
+ val stage = source.getScene().getWindow() as Stage
+ stage.close();
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/PrimaryController.kt b/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/PrimaryController.kt
new file mode 100644
index 0000000..99d1009
--- /dev/null
+++ b/src/main/kotlin/xyz/brysonsteck/ServerCraft/controllers/PrimaryController.kt
@@ -0,0 +1,632 @@
+package xyz.brysonsteck.ServerCraft.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 xyz.brysonsteck.ServerCraft.server.Server
+import xyz.brysonsteck.ServerCraft.server.Download
+import xyz.brysonsteck.ServerCraft.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<kotlin.Int>
+ @FXML
+ lateinit private var difficultyBox: ChoiceBox<String>
+ @FXML
+ lateinit private var gamemodeBox: ChoiceBox<String>
+ @FXML
+ lateinit private var worldTypeBox: ChoiceBox<String>
+ @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<kotlin.Int>
+ @FXML
+ lateinit private var maxSizeSpinner: Spinner<kotlin.Int>
+ @FXML
+ lateinit private var memorySpinner: Spinner<kotlin.Int>
+ @FXML
+ lateinit private var spawnSpinner: Spinner<kotlin.Int>
+ @FXML
+ lateinit private var simulationSpinner: Spinner<kotlin.Int>
+ @FXML
+ lateinit private var renderSpinner: Spinner<kotlin.Int>
+ @FXML
+ lateinit private var maxTickSpinner: Spinner<kotlin.Int>
+ @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 = "<NONE>"
+ 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.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 + "ServerCraft" + File.separator + "Java" + File.separator,
+ "BuildTools" to directory + "ServerCraft" + 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 + "ServerCraft" + File.separator + "Java" + File.separator + javaFile))
+ val dest = File(directory + "ServerCraft" + File.separator + "Java")
+ var entries = 0.0
+ while(stream.getNextEntry() != null && building) {
+ entries++
+ }
+ stream = archiver.stream(File(directory + "ServerCraft" + 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 + "ServerCraft" + 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) {
+ withContext(Dispatchers.JavaFx) {
+ statusBar.text = "Killing Minecraft server..."
+ startButton.isDisable = true
+ }
+ 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.isDisable = false
+ startButton.text = "Start Server"
+ started = false
+ }
+ }
+ }
+
+ private fun eulaDialog(): Boolean {
+ var result = false
+ val resources = App().javaClass.getResource("icons/warning.png")
+ val dialog = Stage()
+ dialog.icons.add(Image(App().javaClass.getResourceAsStream("app.png")))
+ 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<MouseEvent>() {
+ result = false
+ dialog.hide()
+ }
+ noButton.isDefaultButton = true
+ val yesButton = Button("I Agree")
+ yesButton.onMouseClicked = EventHandler<MouseEvent>() {
+ result = true
+ dialog.hide()
+ }
+ val eula = Button("View EULA")
+ eula.onMouseClicked = EventHandler<MouseEvent>() {
+ val desktop = Desktop.getDesktop()
+ if (desktop.isSupported(Desktop.Action.BROWSE)) {
+ // most likely running on Windows or macOS
+ try {
+ desktop.browse(URI("https://account.mojang.com/documents/minecraft_eula"))
+ } catch (e: Exception) {
+ println(e)
+ }
+ } else {
+ // assume running on linux
+ try {
+ Runtime.getRuntime().exec("xdg-open 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 = App().javaClass.getResource("icons/$type.png")
+ val dialog = Stage()
+ dialog.icons.add(Image(App().javaClass.getResourceAsStream("app.png")))
+ 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<MouseEvent>() {
+ if (hold) {
+ result = false
+ } else {
+ asyncResult = false
+ }
+ dialog.hide()
+ }
+ val yesButton = Button(yes)
+ yesButton.onMouseClicked = EventHandler<MouseEvent>() {
+ 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 + "ServerCraft").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 \nServerCraft. 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 + "ServerCraft").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
+ }
+} \ No newline at end of file
diff --git a/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Download.kt b/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Download.kt
new file mode 100644
index 0000000..757c32c
--- /dev/null
+++ b/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Download.kt
@@ -0,0 +1,119 @@
+package xyz.brysonsteck.ServerCraft.server
+
+import java.io.*;
+import java.net.*;
+import java.util.*;
+
+class Download: Runnable {
+ public enum class Status {
+ DOWNLOADING, PAUSED, COMPLETE, CANCELLED, ERROR
+ }
+ public var size: Int
+ public var downloaded: Int
+ public var contentLength: Int
+ public var status: Status
+
+ private final val MAX_BUFFER_SIZE: Number = 1024
+
+ private var url: URL
+ private var dir: String
+
+ constructor (url: URL, dir: String) {
+ this.url = url
+ this.dir = dir
+ size = -1
+ downloaded = 0
+ status = Status.DOWNLOADING
+ contentLength = 1
+ }
+
+ public fun start() {
+ val thread = Thread(this)
+ thread.start()
+ }
+
+ private fun getFilename(url: URL): String {
+ val filename = url.getFile()
+ return filename.substring(filename.lastIndexOf('/') + 1)
+ }
+
+ override fun run() {
+ var stream: InputStream? = null
+ var file: RandomAccessFile? = null
+
+ try {
+ // Open connection to URL.
+ var connection = url.openConnection() as HttpURLConnection;
+
+ // Specify what portion of file to download.
+ connection.setRequestProperty("Range", "bytes=" + downloaded + "-");
+
+ // Connect to server.
+ connection.connect();
+
+ // Make sure response code is in the 200 range.
+ if (connection.responseCode / 100 != 2) {
+ status = Status.ERROR
+ }
+
+ // Check for valid content length.
+ contentLength = connection.getContentLength();
+ if (contentLength < 1) {
+ status = Status.ERROR
+ }
+
+ /* Set the size for this download if it
+ hasn't been already set. */
+ if (size == -1) {
+ size = contentLength;
+ }
+
+ // Open file and seek to the end of it.
+ file = RandomAccessFile(dir + getFilename(url), "rw");
+ file.seek(downloaded.toLong());
+
+ stream = connection.getInputStream();
+ while (status == Status.DOWNLOADING) {
+ /* Size buffer according to how much of the
+ file is left to download. */
+ val buffer: ByteArray;
+ if (size - downloaded > MAX_BUFFER_SIZE as Int) {
+ buffer = ByteArray(MAX_BUFFER_SIZE)
+ } else {
+ buffer = ByteArray(size - downloaded);
+ }
+
+ // Read from server into buffer.
+ val read = stream.read(buffer);
+ if (read == -1)
+ break;
+
+ // Write buffer to file.
+ file.write(buffer, 0, read);
+ downloaded += read;
+ }
+
+ /* Change status to complete if this point was
+ reached because downloading has finished. */
+ if (status == Status.DOWNLOADING) {
+ status = Status.COMPLETE;
+ }
+ } catch (e: Exception) {
+ status = Status.ERROR
+ } finally {
+ // Close file.
+ if (file != null) {
+ try {
+ file.close();
+ } catch (e: Exception) {}
+ }
+
+ // Close connection to server.
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (e: Exception) {}
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Server.kt b/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Server.kt
new file mode 100644
index 0000000..746bc55
--- /dev/null
+++ b/src/main/kotlin/xyz/brysonsteck/ServerCraft/server/Server.kt
@@ -0,0 +1,35 @@
+package xyz.brysonsteck.ServerCraft.server
+
+import java.io.File
+import java.util.Properties
+
+public class Server {
+ public var jar = ""
+
+ private val props = Properties()
+
+ constructor() {
+ props.setProperty("allow-flight", false.toString())
+ props.setProperty("allow-nether", true.toString())
+ props.setProperty("generate-structures", true.toString())
+ props.setProperty("hardcore", false.toString())
+ props.setProperty("pvp", true.toString())
+ props.setProperty("white-list", false.toString())
+ props.setProperty("enable-command-block", false.toString())
+ props.setProperty("hide-online-players", false.toString())
+ props.setProperty("max-players", 20.toString())
+ props.setProperty("max-world-size", 29999984.toString())
+ props.setProperty("server-port", 25565.toString())
+ props.setProperty("view-distance", 10.toString())
+ props.setProperty("jvm-ram", 1024.toString())
+ props.setProperty("spawn-protection", 16.toString())
+ props.setProperty("simulation-distance", 10.toString())
+ props.setProperty("max-tick-time", 60000.toString())
+ props.setProperty("difficulty", "normal")
+ props.setProperty("gamemode", "survival")
+ props.setProperty("level-name", "world")
+ props.setProperty("level-seed", "")
+ props.setProperty("level-type", "minecraft:normal")
+ props.setProperty("motd", "A server for a dummy")
+ }
+} \ No newline at end of file
diff --git a/src/main/resources/dmg.png b/src/main/resources/dmg.png
new file mode 100644
index 0000000..5c33572
--- /dev/null
+++ b/src/main/resources/dmg.png
Binary files differ
diff --git a/src/main/resources/icon.icns b/src/main/resources/icon.icns
new file mode 100644
index 0000000..fe1d98a
--- /dev/null
+++ b/src/main/resources/icon.icns
Binary files differ
diff --git a/src/main/resources/icon.ico b/src/main/resources/icon.ico
new file mode 100644
index 0000000..bcfb4b0
--- /dev/null
+++ b/src/main/resources/icon.ico
Binary files differ
diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png
new file mode 100644
index 0000000..a24c362
--- /dev/null
+++ b/src/main/resources/icon.png
Binary files differ
diff --git a/src/main/resources/xyz/brysonsteck/ServerCraft/app.png b/src/main/resources/xyz/brysonsteck/ServerCraft/app.png
new file mode 100644
index 0000000..ceb633a
--- /dev/null
+++ b/src/main/resources/xyz/brysonsteck/ServerCraft/app.png
Binary files differ
diff --git a/src/main/resources/xyz/brysonsteck/ServerCraft/css/info-tabs.css b/src/main/resources/xyz/brysonsteck/ServerCraft/css/info-tabs.css
new file mode 100644
index 0000000..023b0c1
--- /dev/null
+++ b/src/main/resources/xyz/brysonsteck/ServerCraft/css/info-tabs.css
@@ -0,0 +1,5 @@
+.tab-pane>*.tab-header-area>*.tab-header-background {
+ -fx-background-color: "#F4F4F4";
+ -fx-border-color: "#DCDCDC";
+ -fx-border-width: 0 0 1 0
+} \ No newline at end of file
diff --git a/src/main/resources/xyz/brysonsteck/ServerCraft/icons/info.png b/src/main/resources/xyz/brysonsteck/ServerCraft/icons/info.png
new file mode 100644
index 0000000..c1951a7
--- /dev/null
+++ b/src/main/resources/xyz/brysonsteck/ServerCraft/icons/info.png
Binary files differ
diff --git a/src/main/resources/xyz/brysonsteck/ServerCraft/icons/warning.png b/src/main/resources/xyz/brysonsteck/ServerCraft/icons/warning.png
new file mode 100644
index 0000000..4d66729
--- /dev/null
+++ b/src/main/resources/xyz/brysonsteck/ServerCraft/icons/warning.png
Binary files differ
diff --git a/src/main/resources/xyz/brysonsteck/ServerCraft/info.fxml b/src/main/resources/xyz/brysonsteck/ServerCraft/info.fxml
new file mode 100644
index 0000000..f0b73f3
--- /dev/null
+++ b/src/main/resources/xyz/brysonsteck/ServerCraft/info.fxml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.control.Hyperlink?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.Separator?>
+<?import javafx.scene.control.Tab?>
+<?import javafx.scene.control.TabPane?>
+<?import javafx.scene.image.Image?>
+<?import javafx.scene.image.ImageView?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.Pane?>
+<?import javafx.scene.layout.VBox?>
+<?import javafx.scene.text.Font?>
+
+<Pane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="358.0" prefWidth="398.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="xyz.brysonsteck.ServerCraft.controllers.InfoController">
+ <children>
+ <ImageView fitHeight="115.0" fitWidth="92.0" layoutX="30.0" layoutY="30.0" pickOnBounds="true" preserveRatio="true">
+ <image>
+ <Image url="@app.png" />
+ </image>
+ </ImageView>
+ <Label layoutX="146.0" layoutY="42.0" text="ServerCraft">
+ <font>
+ <Font name="System Bold" size="28.0" />
+ </font>
+ </Label>
+ <Label layoutX="146.0" layoutY="82.0" text="Version 1.0" />
+ <ButtonBar layoutY="318.0" prefHeight="40.0" prefWidth="398.0">
+ <buttons>
+ <Button mnemonicParsing="false" onAction="#closeInfo" text="Close" />
+ </buttons>
+ <padding>
+ <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+ </padding>
+ </ButtonBar>
+ <Label layoutX="52.0" layoutY="135.0" text="A graphical interface for creating Minecraft servers" textAlignment="CENTER" />
+ <TabPane layoutY="158.0" prefHeight="161.0" prefWidth="398.0" stylesheets="@css/info-tabs.css" tabClosingPolicy="UNAVAILABLE">
+ <tabs>
+ <Tab text="About">
+ <content>
+ <Pane prefHeight="200.0" prefWidth="200.0">
+ <children>
+ <Label alignment="TOP_LEFT" prefWidth="398.0" text="This program is for simple Minecraft servers, and expects that the user knows how to port forward.&#10;&#10;ServerCraft is free and open source under the GNU General Public License Version 3.0" wrapText="true">
+ <padding>
+ <Insets bottom="13.0" left="13.0" right="13.0" top="13.0" />
+ </padding>
+ </Label>
+ <Hyperlink fx:id="license" layoutX="127.0" layoutY="110.0" onAction="#openHyperlink" text="License" />
+ <Hyperlink fx:id="source" layoutX="189.0" layoutY="110.0" onAction="#openHyperlink" text="Source Code" />
+ </children>
+ <padding>
+ <Insets bottom="13.0" left="13.0" right="13.0" top="13.0" />
+ </padding>
+ </Pane>
+ </content>
+ </Tab>
+ <Tab text="Authors">
+ <content>
+ <Pane prefHeight="200.0" prefWidth="200.0">
+ <children>
+ <Label layoutX="78.0" layoutY="109.0" text="Want to join the list? Contribute with a PR!" textAlignment="CENTER" />
+ <VBox>
+ <children>
+ <Label text="Bryson Steck" VBox.vgrow="ALWAYS">
+ <font>
+ <Font name="System Bold" size="16.0" />
+ </font>
+ </Label>
+ <Label text="Creator / Maintainer">
+ <padding>
+ <Insets top="4.0" />
+ </padding>
+ </Label>
+ <HBox>
+ <children>
+ <Hyperlink fx:id="bryson_website" onAction="#openHyperlink" text="Website">
+ <HBox.margin>
+ <Insets left="-3.0" top="2.0" />
+ </HBox.margin>
+ </Hyperlink>
+ <Separator orientation="VERTICAL">
+ <padding>
+ <Insets bottom="2.0" top="2.0" />
+ </padding>
+ </Separator>
+ <Hyperlink fx:id="bryson_email" onAction="#openHyperlink" text="Email">
+ <HBox.margin>
+ <Insets top="2.0" />
+ </HBox.margin>
+ </Hyperlink>
+ </children>
+ </HBox>
+ </children>
+ <padding>
+ <Insets bottom="13.0" left="13.0" right="13.0" top="13.0" />
+ </padding>
+ </VBox>
+ </children>
+ </Pane>
+ </content>
+ </Tab>
+ </tabs>
+ </TabPane>
+ </children>
+</Pane>
diff --git a/src/main/resources/xyz/brysonsteck/ServerCraft/primary.fxml b/src/main/resources/xyz/brysonsteck/ServerCraft/primary.fxml
new file mode 100644
index 0000000..6b34102
--- /dev/null
+++ b/src/main/resources/xyz/brysonsteck/ServerCraft/primary.fxml
@@ -0,0 +1,362 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<?import javafx.geometry.Insets?>
+<?import javafx.scene.control.Button?>
+<?import javafx.scene.control.ButtonBar?>
+<?import javafx.scene.control.CheckBox?>
+<?import javafx.scene.control.ChoiceBox?>
+<?import javafx.scene.control.ContextMenu?>
+<?import javafx.scene.control.Label?>
+<?import javafx.scene.control.MenuItem?>
+<?import javafx.scene.control.ProgressBar?>
+<?import javafx.scene.control.Separator?>
+<?import javafx.scene.control.Spinner?>
+<?import javafx.scene.control.SpinnerValueFactory.IntegerSpinnerValueFactory?>
+<?import javafx.scene.control.TextField?>
+<?import javafx.scene.control.TitledPane?>
+<?import javafx.scene.control.Tooltip?>
+<?import javafx.scene.layout.AnchorPane?>
+<?import javafx.scene.layout.HBox?>
+<?import javafx.scene.layout.Pane?>
+<?import javafx.scene.text.Font?>
+
+<Pane fx:id="primary" maxHeight="713.0" maxWidth="963.0" minHeight="713.0" minWidth="963.0" prefHeight="713.0" prefWidth="963.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="xyz.brysonsteck.ServerCraft.controllers.PrimaryController">
+ <children>
+ <HBox fx:id="directoryPane" prefHeight="39.0" prefWidth="963.0">
+ <children>
+ <Button id="openFile" fx:id="chooseDirectoryButton" lineSpacing="10.0" mnemonicParsing="false" onMouseClicked="#onDirectoryButtonClick" text="Choose Directory...">
+ <opaqueInsets>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+ </opaqueInsets>
+ <HBox.margin>
+ <Insets />
+ </HBox.margin>
+ </Button>
+ <Separator orientation="VERTICAL" prefHeight="200.0">
+ <HBox.margin>
+ <Insets left="5.0" />
+ </HBox.margin>
+ </Separator>
+ <Label text="Server Directory:">
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+ </HBox.margin>
+ <font>
+ <Font name="System Bold" size="13.0" />
+ </font>
+ </Label>
+ <Label id="currentFilename" fx:id="currentDirectoryLabel" text="&lt;NONE&gt;">
+ <HBox.margin>
+ <Insets bottom="5.0" right="5.0" top="5.0" />
+ </HBox.margin>
+ </Label>
+ </children>
+ <padding>
+ <Insets bottom="7.0" left="7.0" right="7.0" top="7.0" />
+ </padding>
+ </HBox>
+ <HBox fx:id="worldSettingsPane" disable="true" layoutY="39.0" prefHeight="41.0" prefWidth="963.0">
+ <padding>
+ <Insets bottom="7.0" left="7.0" right="7.0" top="7.0" />
+ </padding>
+ <children>
+ <Label text="World Name:" HBox.hgrow="ALWAYS">
+ <font>
+ <Font name="System Bold" size="13.0" />
+ </font>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="6.0" />
+ </HBox.margin>
+ </Label>
+ <TextField fx:id="worldNameField" onInputMethodTextChanged="#onWorldNameChange" text="world">
+ <HBox.margin>
+ <Insets top="2.0" />
+ </HBox.margin>
+ </TextField>
+ <Separator orientation="VERTICAL" prefHeight="200.0">
+ <HBox.margin>
+ <Insets left="5.0" />
+ </HBox.margin>
+ </Separator>
+ <Label text="Seed:">
+ <font>
+ <Font name="System Bold" size="13.0" />
+ </font>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="6.0" />
+ </HBox.margin>
+ </Label>
+ <TextField fx:id="seedField" onInputMethodTextChanged="#onSeedChange" promptText="Leave empty for random seed" HBox.hgrow="ALWAYS">
+ <HBox.margin>
+ <Insets top="2.0" />
+ </HBox.margin>
+ </TextField>
+ <Separator orientation="VERTICAL" prefHeight="200.0">
+ <HBox.margin>
+ <Insets left="5.0" />
+ </HBox.margin>
+ </Separator>
+ <Label text="Server Port:" HBox.hgrow="ALWAYS">
+ <font>
+ <Font name="System Bold" size="13.0" />
+ </font>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="6.0" />
+ </HBox.margin>
+ </Label>
+ <Spinner fx:id="portSpinner" editable="true" onInputMethodTextChanged="#onPortChange" prefWidth="95.0">
+ <HBox.margin>
+ <Insets top="2.0" />
+ </HBox.margin>
+ <valueFactory>
+ <SpinnerValueFactory.IntegerSpinnerValueFactory amountToStepBy="1" initialValue="25565" max="60000" min="20000" />
+ </valueFactory>
+ </Spinner>
+ </children>
+ <opaqueInsets>
+ <Insets bottom="3.0" />
+ </opaqueInsets>
+ </HBox>
+ <Pane fx:id="parentPane" disable="true" layoutY="78.0" prefHeight="555.0" prefWidth="970.0">
+ <children>
+ <TitledPane fx:id="settingsPane" animated="false" collapsible="false" layoutX="10.0" layoutY="7.0" prefHeight="273.0" prefWidth="627.0" text="Server Settings">
+ <content>
+ <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="311.0" prefWidth="625.0">
+ <children>
+ <CheckBox fx:id="flightCheckbox" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" onMouseClicked="#onCheckboxClick" text="Allow Flight" />
+ <CheckBox fx:id="netherCheckbox" layoutX="14.0" layoutY="42.0" mnemonicParsing="false" onMouseClicked="#onCheckboxClick" selected="true" text="Allow The Nether" />
+ <CheckBox fx:id="structuresCheckbox" alignment="TOP_LEFT" layoutX="14.0" layoutY="70.0" mnemonicParsing="false" onMouseClicked="#onCheckboxClick" selected="true" text="Generate Structures&#10;(such as villages and strongholds)" />
+ <CheckBox fx:id="pvpCheckbox" layoutX="14.0" layoutY="109.0" mnemonicParsing="false" onMouseClicked="#onCheckboxClick" selected="true" text="Allow PvP" />
+ <CheckBox fx:id="whitelistCheckbox" alignment="TOP_LEFT" layoutX="14.0" layoutY="138.0" mnemonicParsing="false" onMouseClicked="#onCheckboxClick" text="Enable Whitelist&#10;(Only users you specify can join)" />
+ <HBox layoutX="6.0" layoutY="174.0">
+ <children>
+ <Label text="Maximum Players:" HBox.hgrow="ALWAYS">
+ <padding>
+ <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+ </padding>
+ </Label>
+ <Spinner fx:id="maxPlayerSpinner" editable="true" prefHeight="23.0" prefWidth="99.0">
+ <valueFactory>
+ <SpinnerValueFactory.IntegerSpinnerValueFactory amountToStepBy="1" initialValue="20" max="1000" min="0" />
+ </valueFactory>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+ </HBox.margin>
+ </Spinner>
+ </children>
+ </HBox>
+ <HBox layoutX="6.0" layoutY="207.0">
+ <children>
+ <Label text="Maximum World Size (in blocks):" HBox.hgrow="ALWAYS">
+ <HBox.margin>
+ <Insets />
+ </HBox.margin>
+ <padding>
+ <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+ </padding>
+ </Label>
+ <Spinner fx:id="maxSizeSpinner" editable="true" prefHeight="23.0" prefWidth="155.0">
+ <valueFactory>
+ <SpinnerValueFactory.IntegerSpinnerValueFactory amountToStepBy="1" initialValue="29999984" max="29999984" min="1" />
+ </valueFactory>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+ </HBox.margin>
+ </Spinner>
+ </children>
+ </HBox>
+ </children>
+ </AnchorPane>
+ </content>
+ </TitledPane>
+ <TitledPane fx:id="advancedPane" expanded="false" layoutX="10.0" layoutY="289.0" prefHeight="259.0" prefWidth="627.0" text="Advanced Server Settings">
+ <content>
+ <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="269.0" prefWidth="625.0">
+ <children>
+ <CheckBox fx:id="cmdBlocksCheckbox" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" onMouseClicked="#onCheckboxClick" text="Enable Command Blocks" />
+ <CheckBox fx:id="playerCountCheckbox" layoutX="14.0" layoutY="41.0" mnemonicParsing="false" onMouseClicked="#onCheckboxClick" text="Hide Online Player Count" />
+ <HBox layoutX="7.0" layoutY="65.0">
+ <children>
+ <Label ellipsisString="" text="Server Memory in MB:" textOverrun="CLIP" HBox.hgrow="ALWAYS">
+ <tooltip>
+ <Tooltip text="This is the amount of RAM that will get passed to Minecraft/the JVM.&#10;For simple servers, 1024 MB will be plenty.&#10;If you typically have more than 5 concurrent players, consider allocating more." />
+ </tooltip>
+ <padding>
+ <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+ </padding>
+ </Label>
+ <Spinner fx:id="memorySpinner" editable="true" prefHeight="23.0" prefWidth="99.0">
+ <valueFactory>
+ <SpinnerValueFactory.IntegerSpinnerValueFactory amountToStepBy="1" initialValue="1024" max="65536" min="512" />
+ </valueFactory>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+ </HBox.margin>
+ </Spinner>
+ </children>
+ </HBox>
+ <HBox layoutX="7.0" layoutY="96.0">
+ <children>
+ <Label text="Spawn Protection Radius:" HBox.hgrow="ALWAYS">
+ <tooltip>
+ <Tooltip text="All blocks in a radius from 0,~,0 will be unbreakable. &#10;If you want to break blocks within spawn, change this value." />
+ </tooltip>
+ <HBox.margin>
+ <Insets />
+ </HBox.margin>
+ <padding>
+ <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+ </padding>
+ </Label>
+ <Spinner fx:id="spawnSpinner" editable="true" prefHeight="23.0" prefWidth="99.0">
+ <valueFactory>
+ <SpinnerValueFactory.IntegerSpinnerValueFactory amountToStepBy="1" initialValue="16" max="29999984" min="0" />
+ </valueFactory>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+ </HBox.margin>
+ </Spinner>
+ </children>
+ </HBox>
+ <HBox layoutX="7.0" layoutY="127.0">
+ <children>
+ <Label text="Simulation Distance:" HBox.hgrow="ALWAYS">
+ <tooltip>
+ <Tooltip text="The radius of chunks for each player where ticks will be updated.&#10;In other words, anything outside these circles, such as furnaces, mobs, etc, will not be updated or simulated." />
+ </tooltip>
+ <HBox.margin>
+ <Insets />
+ </HBox.margin>
+ <padding>
+ <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+ </padding>
+ </Label>
+ <Spinner fx:id="simulationSpinner" editable="true" prefWidth="80.0">
+ <valueFactory>
+ <SpinnerValueFactory.IntegerSpinnerValueFactory amountToStepBy="1" initialValue="10" max="40" min="0" />
+ </valueFactory>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+ </HBox.margin>
+ </Spinner>
+ </children>
+ </HBox>
+ <HBox layoutX="7.0" layoutY="160.0">
+ <children>
+ <Label text="Render Distance:" HBox.hgrow="ALWAYS">
+ <tooltip>
+ <Tooltip text="The radius of chunks where the server will render the view distance.&#10;Any value higher on a client than what is set will be ignored.&#10;Higher values will be more demanding on the server." />
+ </tooltip>
+ <HBox.margin>
+ <Insets />
+ </HBox.margin>
+ <padding>
+ <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+ </padding>
+ </Label>
+ <Spinner fx:id="renderSpinner" editable="true" prefWidth="80.0">
+ <valueFactory>
+ <SpinnerValueFactory.IntegerSpinnerValueFactory amountToStepBy="1" initialValue="10" max="40" min="2" />
+ </valueFactory>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+ </HBox.margin>
+ </Spinner>
+ </children>
+ </HBox>
+ <HBox layoutX="7.0" layoutY="192.0">
+ <children>
+ <Label text="Maximum Tick Time (in milliseconds):" HBox.hgrow="ALWAYS">
+ <tooltip>
+ <Tooltip text="If the server cannot update ticks (i.e. &quot;lags&quot;) for longer than this amount of time, the server will shutdown.&#10;60000 ms (60 seconds) is the default." />
+ </tooltip>
+ <padding>
+ <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+ </padding>
+ </Label>
+ <Spinner fx:id="maxTickSpinner" editable="true">
+ <valueFactory>
+ <SpinnerValueFactory.IntegerSpinnerValueFactory amountToStepBy="1" initialValue="60000" max="180000" min="10000" />
+ </valueFactory>
+ <HBox.margin>
+ <Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
+ </HBox.margin>
+ </Spinner>
+ </children>
+ </HBox>
+ </children>
+ </AnchorPane>
+ </content>
+ </TitledPane>
+ <TitledPane fx:id="difficultyPane" animated="false" collapsible="false" layoutX="649.0" layoutY="7.0" prefHeight="77.0" prefWidth="305.0" text="Difficulty">
+ <content>
+ <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="94.0" prefWidth="303.0">
+ <children>
+ <ChoiceBox fx:id="difficultyBox" layoutX="14.0" layoutY="14.0" prefHeight="23.0" prefWidth="276.0">
+ <contextMenu>
+ <ContextMenu>
+ <items>
+ <MenuItem mnemonicParsing="false" />
+ </items>
+ </ContextMenu>
+ </contextMenu>
+ </ChoiceBox>
+ </children>
+ </AnchorPane>
+ </content>
+ </TitledPane>
+ <TitledPane fx:id="gamemodePane" animated="false" collapsible="false" layoutX="649.0" layoutY="92.0" prefHeight="77.0" prefWidth="305.0" text="Gamemode">
+ <content>
+ <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="94.0" prefWidth="303.0">
+ <children>
+ <ChoiceBox fx:id="gamemodeBox" layoutX="14.0" layoutY="14.0" prefHeight="23.0" prefWidth="276.0" />
+ </children>
+ </AnchorPane>
+ </content>
+ </TitledPane>
+ <TitledPane fx:id="worldTypePane" animated="false" collapsible="false" layoutX="649.0" layoutY="178.0" prefHeight="77.0" prefWidth="305.0" text="World Type">
+ <content>
+ <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="94.0" prefWidth="303.0">
+ <children>
+ <ChoiceBox fx:id="worldTypeBox" layoutX="14.0" layoutY="14.0" prefHeight="23.0" prefWidth="276.0" />
+ </children>
+ </AnchorPane>
+ </content>
+ </TitledPane>
+ </children>
+ </Pane>
+ <ButtonBar fx:id="buttonBar" buttonOrder="L+R" layoutY="635.0" prefHeight="40.0" prefWidth="963.0">
+ <buttons>
+ <Button fx:id="infoButton" mnemonicParsing="false" onMouseClicked="#onInfo" text="About ServerCraft" ButtonBar.buttonData="LEFT" />
+ <Button fx:id="defaultsButton" disable="true" mnemonicParsing="false" onMouseClicked="#onBuild" text="Reset to Defaults" ButtonBar.buttonData="LEFT" />
+ <Button fx:id="buildButton" disable="true" mnemonicParsing="false" onMouseClicked="#onBuild" text="Build Server" ButtonBar.buttonData="RIGHT" />
+ <Button fx:id="startButton" defaultButton="true" disable="true" mnemonicParsing="false" onMouseClicked="#onStart" prefWidth="120.0" text="Start Server" ButtonBar.buttonData="RIGHT" />
+ </buttons>
+ <padding>
+ <Insets bottom="8.0" left="8.0" right="8.0" top="8.0" />
+ </padding>
+ </ButtonBar>
+ <HBox layoutY="680.0" prefHeight="33.0" prefWidth="963.0" style="-fx-background-color: ddd;">
+ <children>
+ <Label text="Status:">
+ <font>
+ <Font name="System Bold" size="13.0" />
+ </font>
+ </Label>
+ <Label fx:id="statusBar" text="Ready.">
+ <HBox.margin>
+ <Insets left="5.0" />
+ </HBox.margin>
+ </Label>
+ <ProgressBar fx:id="progressBar" prefWidth="400.0" visible="false">
+ <HBox.margin>
+ <Insets left="10.0" />
+ </HBox.margin>
+ </ProgressBar>
+ </children>
+ <padding>
+ <Insets bottom="9.0" left="9.0" right="9.0" top="9.0" />
+ </padding>
+ </HBox>
+ </children>
+</Pane>
diff --git a/src/test/kotlin/xyz/brysonsteck/serverfordummies/AppTest.kt b/src/test/kotlin/xyz/brysonsteck/serverfordummies/AppTest.kt
new file mode 100644
index 0000000..aca9105
--- /dev/null
+++ b/src/test/kotlin/xyz/brysonsteck/serverfordummies/AppTest.kt
@@ -0,0 +1,14 @@
+/*
+ * This Kotlin source file was generated by the Gradle 'init' task.
+ */
+package xyz.brysonsteck.ServerCraft
+
+import kotlin.test.Test
+import kotlin.test.assertNotNull
+
+class AppTest {
+ // @Test fun appHasAGreeting() {
+ // val classUnderTest = App()
+ // assertNotNull(classUnderTest.greeting, "app should have a greeting")
+ // }
+}