As a suggestion: I personally think it would be great if you could add some sort of area naming to the npc's so you can categorize them. Other than that the tool looks good!
Here's a little draft i made for them.
[SPOIL]
Category.java
Code:
package app.model;
import java.util.ArrayList;
import java.util.List;
public class Category {
public static final List<String> CATEGORY_LIST = new ArrayList<>();
private final String locationName;
public Category(String locationName) {
this.locationName = locationName;
}
public String getLocationName(){
return this.locationName;
}
}
CategoryParser.java
Code:
package app.io;
import java.util.Scanner;
import app.App;
import app.model.Category;
public class CategoryParser {
public static void parse() {
try(Scanner input = new Scanner(App.class.getResourceAsStream("/categories.txt"))) {
while(input.hasNextLine()) {
String line = input.nextLine();
Category.CATEGORY_LIST.add(line.toUpperCase());
}
}
}
}
MainController.java
Code:
package app.controller;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.ResourceBundle;
import app.io.NpcSpawnParser;
import app.model.Category;
import app.model.Direction;
import app.model.Location;
import app.model.mob.Npc;
import app.model.mob.spawn.NpcSpawn;
import app.scene.ExceptionMessage;
import app.util.FileUtils;
import app.util.MiscUtils;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.collections.transformation.SortedList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Button;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.ComboBox;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableSelectionModel;
import javafx.scene.control.TableView;
import javafx.scene.control.TableView.TableViewSelectionModel;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.FileChooser;
import javafx.stage.FileChooser.ExtensionFilter;
public class MainController implements Initializable {
@FXML
private TextField idTf, xTf, yTf, heightTf;
@FXML
private TextField nameTf;
@FXML
private TextField searchTf;
@FXML
private TableView<NpcSpawn> tableView;
@FXML
private TableColumn<NpcSpawn, Integer> idCol;
@FXML
private TableColumn<NpcSpawn, String> nameCol;
@FXML
private ComboBox<Boolean> randomWalkCb;
@FXML
private ComboBox<Direction> directionCb;
@FXML
private ComboBox<String> categoryCb;
@FXML
private MenuItem openMI, closeMI, creditMI;
@FXML
private MenuBar menuBar;
private ObservableList<Boolean> randomWalk = FXCollections.observableArrayList(true, false);
private ObservableList<Direction> direction = FXCollections.observableArrayList(Direction.values());
private ObservableList<String> categories = FXCollections.observableArrayList(Category.CATEGORY_LIST);
private File spawnFile, currentPath;
@FXML
private Button entryBtn, deleteBtn, saveBtn, clearBtn, writeBtn, resetBtn, openBtn;
public ObservableList<NpcSpawn> spawnData = FXCollections.observableArrayList();
private FilteredList<NpcSpawn> filteredData;
private SortedList<NpcSpawn> sortedData;
@Override
public void initialize(URL location, ResourceBundle resources) {
idCol.setCellValueFactory(new PropertyValueFactory<NpcSpawn, Integer>("id"));
nameCol.setCellValueFactory(new PropertyValueFactory<NpcSpawn, String>("name"));
randomWalkCb.setItems(randomWalk);
directionCb.setItems(direction);
categoryCb.setItems(categories);
filteredData = new FilteredList<>(spawnData, p -> true);
searchTf.textProperty().addListener((observable, oldValue, newValue) -> {
filteredData.setPredicate(spawn -> {
if (newValue == null || newValue.isEmpty()) {
return true;
}
String lowerCaseFilter = newValue.toLowerCase();
if (spawn.getName().toLowerCase().contains(lowerCaseFilter)) {
return true;
}
return false;
});
});
sortedData = new SortedList<>(filteredData);
sortedData.comparatorProperty().bind(tableView.comparatorProperty());
tableView.getSelectionModel().selectedItemProperty()
.addListener(new ChangeListener<Object>() {
@Override
public void changed(ObservableValue<?> value, Object ov, Object nv) {
TableSelectionModel<NpcSpawn> selectionModel =
tableView.getSelectionModel();
ObservableList<NpcSpawn> selectedItem =
selectionModel.getSelectedItems();
NpcSpawn spawn = selectedItem.get(0);
if (spawn != null) {
idTf.setText(Integer.toString(spawn.getId()));
nameTf.setText(spawn.getName());
xTf.setText(Integer.toString(spawn.getLocation().getX()));
yTf.setText(Integer.toString(spawn.getLocation().getY()));
heightTf.setText(Integer
.toString(spawn.getLocation().getHeight()));
randomWalkCb.setValue(spawn.getRandomWalk());
directionCb.setValue(spawn.getFacing());
} else {
clearForm();
}
}
});
tableView.setItems(sortedData);
Image writeIcon = new Image(getClass().getResourceAsStream("/write_icon.png"));
Image addIcon = new Image(getClass().getResourceAsStream("/add_icon.png"));
Image removeIcon = new Image(getClass().getResourceAsStream("/remove_icon.png"));
Image saveIcon = new Image(getClass().getResourceAsStream("/save_icon.png"));
Image clearIcon = new Image(getClass().getResourceAsStream("/clear_icon.png"));
Image resetIcon = new Image(getClass().getResourceAsStream("/reset_icon.png"));
Image openIcon = new Image(getClass().getResourceAsStream("/open_icon.png"));
writeBtn.setGraphic(new ImageView(writeIcon));
entryBtn.setGraphic(new ImageView(addIcon));
deleteBtn.setGraphic(new ImageView(removeIcon));
saveBtn.setGraphic(new ImageView(saveIcon));
clearBtn.setGraphic(new ImageView(clearIcon));
resetBtn.setGraphic(new ImageView(resetIcon));
openBtn.setGraphic(new ImageView(openIcon));
}
@FXML
private void close() {
System.exit(0);
}
@FXML
private void reset() {
clearForm();
NpcSpawn.SPAWNS.clear();
Npc.NPC_LIST.clear();
Category.CATEGORY_LIST.clear();
spawnData.clear();
}
@FXML
private void addEntry(ActionEvent event) {
NpcSpawn entry = new NpcSpawn(0, new Location(0, 0, 0));
spawnData.add(entry);
clearForm();
}
@FXML
private void deleteEntry(ActionEvent event) {
TableViewSelectionModel<NpcSpawn> selectionModel = tableView.getSelectionModel();
ObservableList<NpcSpawn> selectedCells = selectionModel.getSelectedItems();
NpcSpawn spawn = selectedCells.get(0);
if (spawn != null) {
spawnData.remove(spawn);
} else {
clearForm();
}
}
@FXML
private boolean saveItem() {
TableViewSelectionModel<NpcSpawn> selectionModel = tableView.getSelectionModel();
ObservableList<NpcSpawn> selectedCells = selectionModel.getSelectedItems();
NpcSpawn spawn = selectedCells.get(0);
if (spawn != null) {
if (!validText("[0-9]+", idTf, xTf, yTf, heightTf) || !validText("[a-zA-z ]+", nameTf)) {
Alert alert = new Alert(AlertType.WARNING);
alert.setTitle("Warning!");
alert.setHeaderText("Warning");
alert.setContentText("There's invalid text in a textfield, could not save.");
alert.showAndWait();
return false;
}
spawn.setId(Integer.parseInt(idTf.getText()));
spawn.setName(nameTf.getText());
spawn.setLocation(new Location(Integer.parseInt(xTf.getText()),
Integer.parseInt(yTf.getText()),
Integer.parseInt(heightTf.getText())));
spawn.setRandomWalk(randomWalkCb.getValue());
spawn.setFacing(directionCb.getValue());
refreshColumns();
return true;
}
return false;
}
@FXML
private void serialize() {
if (spawnData.isEmpty()) {
return;
}
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save File");
Path homePath = Paths.get(System.getProperty("user.home"));
if (currentPath != null) {
homePath = currentPath.getParentFile().toPath();
}
File file = homePath.toFile();
fileChooser.setInitialDirectory(file);
fileChooser.getExtensionFilters().add(new ExtensionFilter("Json Files", "*.json"));
file = fileChooser.showSaveDialog(menuBar.getScene().getWindow());
if (file != null) {
String filePath = file.getAbsolutePath();
try {
FileUtils.serialize(filePath, spawnData);
} catch (IOException ex) {
new ExceptionMessage("A problem was encountered while writing data.", ex);
}
}
}
@FXML
private void refreshColumns() {
tableView.getColumns().get(0).setVisible(false);
tableView.getColumns().get(0).setVisible(true);
}
@FXML
private void clearForm() {
idTf.setText("0");
nameTf.setText("Unknown");
xTf.setText("0");
yTf.setText("0");
heightTf.setText("0");
randomWalkCb.setItems(randomWalk);
randomWalkCb.setValue(true);
directionCb.setValue(Direction.SOUTH);
categoryCb.setValue("NONE");
}
@FXML
private void openFile() {
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Open Resource File");
String homePath = System.getProperty("user.home");
File file = new File(homePath);
fileChooser.setInitialDirectory(file);
try {
fileChooser.getExtensionFilters()
.add(new ExtensionFilter("Json Files", "*.json"));
spawnFile = fileChooser.showOpenDialog(menuBar.getScene().getWindow());
currentPath = spawnFile;
} catch (Exception ex) {
new ExceptionMessage("A problem was encountered while trying to open a file.",
ex);
}
if (spawnFile != null) {
if (!spawnFile.getName().contains("npc_spawns")) {
return;
}
try {
new NpcSpawnParser(spawnFile.toPath().toString().replaceAll(".json", ""))
.run();
spawnData = FXCollections.observableArrayList(NpcSpawn.SPAWNS);
filteredData = new FilteredList<>(spawnData, p -> true);
sortedData = new SortedList<>(filteredData);
sortedData.comparatorProperty().bind(tableView.comparatorProperty());
tableView.setItems(sortedData);
} catch (Exception ex) {
new ExceptionMessage(
"A problem was encountered while loading the spawn file.", ex);
}
}
}
@FXML
private void showCredits() {
MiscUtils.launchURL("https://www.rune-server.org/members/seven/");
}
private final boolean validText(String regex, TextField... input) {
int size = input.length;
for(int index = 0; index < size; index++) {
if (!input[index].getText().matches(regex)) {
return false;
}
}
return true;
}
}
Spawn.fxml
Code:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.control.ToolBar?>
<?import javafx.scene.control.Tooltip?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<BorderPane prefHeight="421.0" prefWidth="717.0" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1" fx:controller="app.controller.MainController">
<left>
<VBox alignment="TOP_CENTER" prefHeight="421.0" prefWidth="258.0" BorderPane.alignment="CENTER">
<children>
<TableView fx:id="tableView" prefHeight="351.0" prefWidth="242.0">
<columns>
<TableColumn fx:id="idCol" prefWidth="75.0" text="Id" />
<TableColumn fx:id="nameCol" prefWidth="182.0" text="Name" />
</columns>
<VBox.margin>
<Insets left="5.0" right="5.0" />
</VBox.margin>
</TableView>
<TextField fx:id="searchTf" alignment="CENTER" promptText="Search....">
<VBox.margin>
<Insets left="5.0" right="5.0" top="5.0" />
</VBox.margin></TextField>
<HBox alignment="CENTER" prefHeight="0.0" prefWidth="221.0" VBox.vgrow="NEVER">
<children>
<Button fx:id="entryBtn" mnemonicParsing="false" onAction="#addEntry" prefWidth="80.0">
<HBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</HBox.margin>
<tooltip>
<Tooltip text="Add" />
</tooltip></Button>
<Button fx:id="deleteBtn" mnemonicParsing="false" onAction="#deleteEntry" prefWidth="80.0">
<HBox.margin>
<Insets bottom="5.0" right="5.0" top="5.0" />
</HBox.margin>
<tooltip>
<Tooltip text="Remove" />
</tooltip></Button>
</children>
</HBox>
</children>
</VBox>
</left>
<center>
<VBox alignment="TOP_CENTER" prefHeight="200.0" prefWidth="100.0" BorderPane.alignment="CENTER">
<children>
<TitledPane prefHeight="337.0" prefWidth="459.0" text="Spawn Information" VBox.vgrow="ALWAYS">
<content>
<GridPane prefHeight="135.0" prefWidth="373.0">
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" />
<ColumnConstraints hgrow="SOMETIMES" />
</columnConstraints>
<rowConstraints>
<RowConstraints />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
</rowConstraints>
<children>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" GridPane.rowIndex="1">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Id">
<VBox.margin>
<Insets left="5.0" right="5.0" top="5.0" />
</VBox.margin></Text>
<TextField fx:id="idTf" alignment="CENTER" promptText="No Value">
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin></TextField>
</children>
</VBox>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" GridPane.columnIndex="1" GridPane.rowIndex="1">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Name">
<VBox.margin>
<Insets left="5.0" right="5.0" top="5.0" />
</VBox.margin></Text>
<TextField fx:id="nameTf" alignment="CENTER" promptText="No Value">
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin></TextField>
</children>
</VBox>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" GridPane.rowIndex="2">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="X">
<VBox.margin>
<Insets left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</Text>
<TextField alignment="CENTER" promptText="No Value" fx:id="xTf">
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin></TextField>
</children>
</VBox>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" GridPane.columnIndex="1" GridPane.rowIndex="2">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Y">
<VBox.margin>
<Insets left="5.0" right="5.0" top="5.0" />
</VBox.margin></Text>
<TextField fx:id="yTf" alignment="CENTER" promptText="No Value">
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin></TextField>
</children>
</VBox>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" GridPane.rowIndex="3">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Height">
<VBox.margin>
<Insets left="5.0" right="5.0" top="5.0" />
</VBox.margin></Text>
<TextField fx:id="heightTf" alignment="CENTER" promptText="No Value">
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin></TextField>
</children>
</VBox>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" GridPane.columnIndex="1" GridPane.rowIndex="3">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Random Walk">
<VBox.margin>
<Insets left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</Text>
<ComboBox fx:id="randomWalkCb" prefWidth="150.0" promptText="true">
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</ComboBox>
</children>
</VBox>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" GridPane.rowIndex="4">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Facing Direction">
<VBox.margin>
<Insets left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</Text>
<ComboBox fx:id="directionCb" prefWidth="150.0" promptText="SOUTH">
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</ComboBox>
</children>
</VBox>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0" GridPane.columnIndex="1" GridPane.rowIndex="4">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Category">
<VBox.margin>
<Insets left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</Text>
<ComboBox fx:id="categoryCb" prefWidth="150.0" promptText="NONE">
<VBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</VBox.margin>
</ComboBox>
</children>
</VBox>
</children>
</GridPane>
</content>
<VBox.margin>
<Insets right="5.0" />
</VBox.margin>
</TitledPane>
<HBox alignment="CENTER" prefHeight="29.0" prefWidth="459.0" VBox.vgrow="NEVER">
<children>
<Button fx:id="saveBtn" mnemonicParsing="false" onAction="#saveItem" prefWidth="100.0">
<HBox.margin>
<Insets bottom="5.0" left="5.0" right="5.0" top="5.0" />
</HBox.margin>
<tooltip>
<Tooltip text="Save" />
</tooltip>
</Button>
<Button fx:id="clearBtn" mnemonicParsing="false" onAction="#clearForm" prefWidth="100.0">
<HBox.margin>
<Insets bottom="5.0" right="5.0" top="5.0" />
</HBox.margin>
<tooltip>
<Tooltip text="Clear" />
</tooltip>
</Button>
</children>
</HBox>
</children>
</VBox>
</center>
<top>
<VBox alignment="CENTER">
<children>
<MenuBar fx:id="menuBar" BorderPane.alignment="CENTER">
<menus>
<Menu mnemonicParsing="false" text="File">
<items>
<MenuItem fx:id="openMI" mnemonicParsing="false" onAction="#openFile" text="Open" />
<MenuItem fx:id="closeMI" mnemonicParsing="false" onAction="#close" text="Close" />
</items>
</Menu>
<Menu mnemonicParsing="false" text="Credits">
<items>
<MenuItem fx:id="creditMI" mnemonicParsing="false" onAction="#showCredits" text="Seven" />
</items>
</Menu>
</menus>
</MenuBar>
<ToolBar prefHeight="40.0" prefWidth="200.0">
<items>
<Button fx:id="openBtn" mnemonicParsing="false" onAction="#openFile" prefWidth="100.0" />
<Button fx:id="writeBtn" mnemonicParsing="false" onAction="#serialize" prefWidth="100.0">
<tooltip>
<Tooltip text="Save to a file" />
</tooltip></Button>
<Button fx:id="resetBtn" mnemonicParsing="false" onAction="#reset" prefWidth="100.0" />
</items>
</ToolBar>
</children>
</VBox>
</top>
</BorderPane>
Example of resources/categories.txt
Code:
gwd_armadyl_eyrie
gwd_bandos_stronghold
gwd_saradomin_encampment
gwd_zamorak_fortress
TODO: add to serialization/deserialization.[/SPOIL]