implemented properties, console, and logging system

This commit is contained in:
Bryson Steck 2023-05-20 20:02:08 -06:00
parent 9b200a05ef
commit 49311129e9
7 changed files with 299 additions and 76 deletions

View file

@ -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 {

View file

@ -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<kotlin.Int>
lateinit private var maxPlayerSpinner: Spinner<kotlin.Int>
@FXML
lateinit private var maxSizeSpinner: Spinner<kotlin.Int>
@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 = "<NONE>"
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
}
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 onSeedChange() {
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)
}
}
}
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 onPortChange() {
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
}
@FXML
private fun onCheckboxClick() {
}
@FXML
private fun onSpinnerChange() {
}
private fun onChoiceBoxChange(box: String, selection: String) {
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<MouseEvent>() {
noButton.onAction = EventHandler<ActionEvent>() {
result = false
dialog.hide()
}
noButton.isDefaultButton = true
val yesButton = Button("I Agree")
yesButton.onMouseClicked = EventHandler<MouseEvent>() {
yesButton.onAction = EventHandler<ActionEvent>() {
result = true
dialog.hide()
}
val eula = Button("View EULA")
eula.onMouseClicked = EventHandler<MouseEvent>() {
eula.onAction = EventHandler<ActionEvent>() {
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<MouseEvent>() {
noButton.onAction = EventHandler<ActionEvent>() {
if (hold) {
result = false
} else {
@ -536,7 +699,7 @@ class PrimaryController {
dialog.hide()
}
val yesButton = Button(yes)
yesButton.onMouseClicked = EventHandler<MouseEvent>() {
yesButton.onAction = EventHandler<ActionEvent>() {
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;
}

View file

@ -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.

View file

@ -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()
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 841 B

View file

@ -9,22 +9,25 @@
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.ScrollPane?>
<?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.image.Image?>
<?import javafx.scene.image.ImageView?>
<?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">
<Pane fx:id="primary" maxHeight="873.0" maxWidth="963.0" minHeight="713.0" minWidth="963.0" prefHeight="873.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...">
<Button id="openFile" fx:id="chooseDirectoryButton" lineSpacing="10.0" mnemonicParsing="false" onAction="#onDirectoryButtonClick" text="Choose Directory...">
<opaqueInsets>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</opaqueInsets>
@ -68,7 +71,7 @@
<Insets bottom="5.0" left="5.0" right="5.0" top="6.0" />
</HBox.margin>
</Label>
<TextField fx:id="worldNameField" onInputMethodTextChanged="#onWorldNameChange" text="world">
<TextField fx:id="worldNameField" text="world">
<HBox.margin>
<Insets top="2.0" />
</HBox.margin>
@ -86,7 +89,7 @@
<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">
<TextField fx:id="seedField" promptText="Leave empty for random seed" HBox.hgrow="ALWAYS">
<HBox.margin>
<Insets top="2.0" />
</HBox.margin>
@ -104,7 +107,7 @@
<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">
<Spinner fx:id="portSpinner" editable="true" prefWidth="95.0">
<HBox.margin>
<Insets top="2.0" />
</HBox.margin>
@ -123,11 +126,11 @@
<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)" />
<CheckBox fx:id="flightCheckbox" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" onAction="#onCheckboxClick" text="Allow Flight" />
<CheckBox fx:id="netherCheckbox" layoutX="14.0" layoutY="42.0" mnemonicParsing="false" onAction="#onCheckboxClick" selected="true" text="Allow The Nether" />
<CheckBox fx:id="structuresCheckbox" alignment="TOP_LEFT" layoutX="14.0" layoutY="70.0" mnemonicParsing="false" onAction="#onCheckboxClick" selected="true" text="Generate Structures&#10;(such as villages and strongholds)" />
<CheckBox fx:id="pvpCheckbox" layoutX="14.0" layoutY="109.0" mnemonicParsing="false" onAction="#onCheckboxClick" selected="true" text="Allow PvP" />
<CheckBox fx:id="whitelistCheckbox" alignment="TOP_LEFT" layoutX="14.0" layoutY="138.0" mnemonicParsing="false" onAction="#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">
@ -173,8 +176,8 @@
<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" />
<CheckBox fx:id="cmdBlocksCheckbox" layoutX="14.0" layoutY="14.0" mnemonicParsing="false" onAction="#onCheckboxClick" text="Enable Command Blocks" />
<CheckBox fx:id="playerCountCheckbox" layoutX="14.0" layoutY="41.0" mnemonicParsing="false" onAction="#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">
@ -327,36 +330,57 @@
</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" />
<Button fx:id="infoButton" mnemonicParsing="false" onAction="#onInfo" text="About ServerCraft" ButtonBar.buttonData="LEFT" />
<Button fx:id="defaultsButton" disable="true" mnemonicParsing="false" onAction="#onDefaults" text="Reset to Defaults" ButtonBar.buttonData="LEFT" />
<Button fx:id="buildButton" disable="true" mnemonicParsing="false" onAction="#onBuild" text="Build Server" ButtonBar.buttonData="RIGHT" />
<Button fx:id="startButton" defaultButton="true" disable="true" mnemonicParsing="false" onAction="#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;">
<Pane layoutY="680.0" prefHeight="196.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>
<HBox prefWidth="963.0">
<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>
<ImageView fx:id="dropDownIcon" fitHeight="66.0" fitWidth="39.0" layoutX="923.0" layoutY="-3.0" onMouseClicked="#onToggleConsole" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="@icons/arrow_down.png" />
</image>
</ImageView>
<ScrollPane fx:id="scrollPane" layoutY="34.0" prefHeight="162.0" prefWidth="963.0">
<padding>
<Insets bottom="7.0" left="7.0" right="7.0" top="7.0" />
</padding>
<content>
<Label fx:id="console" prefWidth="935.0" text="Console Output:&#10; " wrapText="true">
<font>
<Font name="Monospaced Regular" size="13.0" />
</font>
</Label>
</content>
</ScrollPane>
</children>
<padding>
<Insets bottom="9.0" left="9.0" right="9.0" top="9.0" />
</padding>
</HBox>
</Pane>
</children>
</Pane>