From af4b79a14b1744322f62e28c9f710ca06b28d21b Mon Sep 17 00:00:00 2001 From: ceikry Date: Fri, 16 Jul 2021 14:35:49 -0500 Subject: [PATCH] Added v1.0 --- build.gradle | 37 +++ gradle.properties | 1 + settings.gradle | 2 + src/main/kotlin/DropTableEditor.kt | 363 +++++++++++++++++++++++++++++ src/main/kotlin/EditorConstants.kt | 21 ++ src/main/kotlin/EditorData.kt | 19 ++ src/main/kotlin/Editors.kt | 269 +++++++++++++++++++++ src/main/kotlin/FileLoader.kt | 106 +++++++++ src/main/kotlin/Globals.kt | 57 +++++ src/main/kotlin/ItemMenu.kt | 100 ++++++++ src/main/kotlin/Logger.kt | 21 ++ src/main/kotlin/MainScreen.kt | 143 ++++++++++++ src/main/kotlin/ShopEdit.kt | 170 ++++++++++++++ src/main/kotlin/ShopList.kt | 131 +++++++++++ src/main/kotlin/TableData.kt | 33 +++ src/main/kotlin/TableEditor.kt | 152 ++++++++++++ src/main/kotlin/TableList.kt | 126 ++++++++++ src/main/resources/thanos.png | Bin 0 -> 50080 bytes 18 files changed, 1751 insertions(+) create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 settings.gradle create mode 100644 src/main/kotlin/DropTableEditor.kt create mode 100644 src/main/kotlin/EditorConstants.kt create mode 100644 src/main/kotlin/EditorData.kt create mode 100644 src/main/kotlin/Editors.kt create mode 100644 src/main/kotlin/FileLoader.kt create mode 100644 src/main/kotlin/Globals.kt create mode 100644 src/main/kotlin/ItemMenu.kt create mode 100644 src/main/kotlin/Logger.kt create mode 100644 src/main/kotlin/MainScreen.kt create mode 100644 src/main/kotlin/ShopEdit.kt create mode 100644 src/main/kotlin/ShopList.kt create mode 100644 src/main/kotlin/TableData.kt create mode 100644 src/main/kotlin/TableEditor.kt create mode 100644 src/main/kotlin/TableList.kt create mode 100644 src/main/resources/thanos.png diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..ab5919e --- /dev/null +++ b/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'org.jetbrains.kotlin.jvm' version '1.4.32' +} + +group 'org.example' +version '1.0-SNAPSHOT' + +repositories { + repositories { + mavenCentral() + } +} + +dependencies { + implementation "org.jetbrains:markdown-jvm:0.2.0" + implementation "com.github.ajalt.mordant:mordant:2.0.0-alpha2" + implementation "com.github.weisj:darklaf-core:2.5.5" + implementation "com.googlecode.json-simple:json-simple:1.1.1" + implementation "org.jetbrains.kotlin:kotlin-stdlib" +} + +task buildJar(type: Jar) { + manifest { + attributes( + 'Main-Class': 'MainScreenKt' + ) + } + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } + { + exclude "META-INF/*.SF" + exclude "META-INF/*.DSA" + exclude "META-INF/*.RSA" + } + with jar +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..d59ae17 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'rs09-thanos-tool' + diff --git a/src/main/kotlin/DropTableEditor.kt b/src/main/kotlin/DropTableEditor.kt new file mode 100644 index 0000000..d761e86 --- /dev/null +++ b/src/main/kotlin/DropTableEditor.kt @@ -0,0 +1,363 @@ +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.* +import java.math.BigDecimal +import java.math.RoundingMode +import java.util.* +import javax.swing.* +import javax.swing.event.ChangeEvent +import javax.swing.table.DefaultTableModel + +class DropTableEditor(val table: NPCDropTable) : JFrame("Editing ${if(table.ids.isNotBlank()) TableData.npcNames[table.ids.split(",")[0].toInt()] ?: "Unknown Table" else "None"}") { + val IDsField = JTextField() + val descriptionField = JTextField() + val mainModel = object : DefaultTableModel(){} + val defaultModel = object : DefaultTableModel(){} + val charmModel = object : DefaultTableModel(){} + + val mainTable = object : JTable(mainModel){ + override fun editCellAt(row: Int, column: Int): Boolean { + if(column == 0 || column == 1){ + ItemMenu.caller = {id,name -> + model.setValueAt(id,row,0) + model.setValueAt(name,row,1) + table[row].id = id + } + ItemMenu.open() + return false + } + return super.editCellAt(row, column) + } + + override fun editCellAt(row: Int, column: Int, e: EventObject?): Boolean { + if(column == 0 || column == 1){ + ItemMenu.caller = {id,name -> + model.setValueAt(id,row,0) + model.setValueAt(name,row,1) + table[row].id = id + } + ItemMenu.open() + return false + } + return super.editCellAt(row, column, e) + } + override fun editingStopped(e: ChangeEvent?) { + val editor = cellEditor + val newValue = editor.cellEditorValue.toString() + when(editingColumn){ + 2 -> table[editingRow].minAmt = newValue + 3 -> table[editingRow].maxAmt = newValue + 4 -> { + table.totalWeight += newValue.toDouble() - table[editingRow].weight + table[editingRow].weight = newValue.toDouble() + updateMainPercentages() + } + } + super.editingStopped(e) + } + } + val mainScrollpane = JScrollPane(mainTable) + val mainSubtable = Subtable("Main",mainScrollpane,table,mainModel) + + val defaultTable = object : JTable(defaultModel){ + override fun editCellAt(row: Int, column: Int): Boolean { + if(column == 0 || column == 1){ + ItemMenu.caller = {id,name -> + model.setValueAt(id,row,0) + model.setValueAt(name,row,1) + table.alwaysTable[row].id = id + } + ItemMenu.open() + return false + } + return super.editCellAt(row, column) + } + + override fun editCellAt(row: Int, column: Int, e: EventObject?): Boolean { + if(column == 0 || column == 1){ + ItemMenu.caller = {id,name -> + model.setValueAt(id,row,0) + model.setValueAt(name,row,1) + table.alwaysTable[row].id = id + } + ItemMenu.open() + return false + } + return super.editCellAt(row, column, e) + } + override fun editingStopped(e: ChangeEvent?) { + val editor = cellEditor + val newValue = editor.cellEditorValue.toString() + when(editingColumn){ + 2 -> table.alwaysTable[editingRow].minAmt = newValue + 3 -> table.alwaysTable[editingRow].maxAmt = newValue + } + super.editingStopped(e) + } + } + val defaultScrollPane = JScrollPane(defaultTable) + val defaultSubtable = Subtable("Always",defaultScrollPane,table.alwaysTable,defaultModel) + + val charmTable = object : JTable(charmModel){ + override fun editCellAt(row: Int, column: Int): Boolean { + if(column == 0 || column == 1){ + ItemMenu.caller = {id,name -> + model.setValueAt(id,row,0) + model.setValueAt(name,row,1) + table.charmTable[row].id = id + } + ItemMenu.open() + return false + } + return super.editCellAt(row, column) + } + + override fun editCellAt(row: Int, column: Int, e: EventObject?): Boolean { + if(column == 0 || column == 1){ + ItemMenu.caller = {id,name -> + model.setValueAt(id,row,0) + model.setValueAt(name,row,1) + table.charmTable[row].id = id + } + ItemMenu.open() + return false + } + return super.editCellAt(row, column, e) + } + override fun editingStopped(e: ChangeEvent?) { + val editor = cellEditor + val newValue = editor.cellEditorValue.toString() + when(editingColumn){ + 2 -> table.charmTable[editingRow].minAmt = newValue + 3 -> table.charmTable[editingRow].maxAmt = newValue + 4 -> { + table.charmTable.totalWeight += newValue.toDouble() - table.charmTable[editingRow].weight + table.charmTable[editingRow].weight = newValue.toDouble() + updateCharmPercentages() + } + } + super.editingStopped(e) + } + } + val charmScrollPane = JScrollPane(charmTable) + val charmSubtable = Subtable("Charm",charmScrollPane,table.charmTable,charmModel) + + init { + layout = BorderLayout() + val topPanel = JPanel() + val IDsLabel = JLabel("IDs: ") + IDsField.preferredSize = Dimension(200,20) + IDsField.addKeyListener(object : KeyListener { + override fun keyTyped(e: KeyEvent?) {} + override fun keyPressed(e: KeyEvent?) {} + override fun keyReleased(e: KeyEvent?) { + table.ids = IDsField.text + } + }) + IDsField.text = table.ids + descriptionField.preferredSize = Dimension(400,20) + descriptionField.text = table.description + descriptionField.addKeyListener(object : KeyListener{ + override fun keyTyped(e: KeyEvent?) {} + override fun keyPressed(e: KeyEvent?) {} + override fun keyReleased(e: KeyEvent?) { + table.description = descriptionField.text + } + }) + topPanel.add(IDsLabel) + topPanel.add(IDsField) + + val mainButton = JButton("Edit Main") + mainButton.addActionListener { + mainSubtable.open() + } + topPanel.add(mainButton) + + val charmsButton = JButton("Edit Charms") + charmsButton.addActionListener { + charmSubtable.open() + } + topPanel.add(charmsButton) + + val defaultButton = JButton("Edit Default") + defaultButton.addActionListener { + defaultSubtable.open() + } + topPanel.add(defaultButton) + + add(topPanel,BorderLayout.NORTH) + + val descriptionPanel = JPanel() + val descriptionLabel = JLabel("Description: ") + descriptionPanel.add(descriptionLabel) + descriptionPanel.add(descriptionField) + + add(descriptionPanel,BorderLayout.SOUTH) + + mainModel.addColumn("ID") + mainModel.addColumn("Name") + mainModel.addColumn("Min Amt") + mainModel.addColumn("Max Amt") + mainModel.addColumn("Weight") + mainModel.addColumn("(%)") + + defaultModel.addColumn("ID") + defaultModel.addColumn("Name") + defaultModel.addColumn("Min Amt") + defaultModel.addColumn("Max Amt") + + charmModel.addColumn("ID") + charmModel.addColumn("Name") + charmModel.addColumn("Min Amt") + charmModel.addColumn("Max Amt") + charmModel.addColumn("Weight") + charmModel.addColumn("(%)") + + mainTable.inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete") + mainTable.actionMap.put("delete",object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + try { + table.totalWeight -= table[mainTable.selectedRow].weight + table.removeAt(mainTable.selectedRow) + mainModel.removeRow(mainTable.selectedRow) + updateMainPercentages() + } catch(e: Exception){ + Logger.logErr("Tried to remove nonexistent row ${mainTable.selectedRow}") + } + } + }) + + defaultTable.inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete") + defaultTable.actionMap.put("delete",object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + try { + table.alwaysTable.removeAt(defaultTable.selectedRow) + defaultModel.removeRow(defaultTable.selectedRow) + } catch(e: Exception){ + Logger.logErr("Tried to remove nonexistent row ${defaultTable.selectedRow}") + } + } + }) + + charmTable.inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete") + charmTable.actionMap.put("delete",object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + try { + table.totalWeight -= table.charmTable[charmTable.selectedRow].weight + table.charmTable.removeAt(charmTable.selectedRow) + charmModel.removeRow(charmTable.selectedRow) + updateCharmPercentages() + } catch(e: Exception){ + Logger.logErr("Tried to remove nonexistent row ${charmTable.selectedRow}") + } + } + }) + + addWindowListener(object : WindowAdapter(){ + override fun windowClosing(e: WindowEvent?) { + FileLoader.dropTableList?.populated = false + FileLoader.dropTableList?.dispatchEvent(WindowEvent(FileLoader.dropTableList!!, WindowEvent.WINDOW_CLOSING)) + FileLoader.dropTableList?.open() + super.windowClosing(e) + } + }) + + pack() + + isVisible = false + } + + class Subtable(val label: String, val pane: JScrollPane, val table: WeightBasedTable, val model: DefaultTableModel) : JFrame("Editing $label Drops"){ + init { + layout = BorderLayout() + + val panel = JPanel() + panel.layout = BorderLayout() + val topPanel = JPanel() + + val mainLabel = JLabel("$label Drops") + topPanel.add(mainLabel) + val addButton = JButton("Add Drop") + addButton.addActionListener { + ItemMenu.caller = {id,name -> + when(label){ + "Always" -> { + model.addRow(arrayOf(id,name,1,1)) + table.add(WeightedItem(id,"1","1",1.0,true)) + } + else -> { + model.addRow(arrayOf(id,name,1,1,1.0,"")) + table.add(WeightedItem(id,"1","1",1.0,false)) + updatePercentages(model) + } + } + } + ItemMenu.open() + } + topPanel.add(addButton) + + panel.add(topPanel,BorderLayout.NORTH) + panel.add(pane,BorderLayout.SOUTH) + + add(panel) + pack() + minimumSize = Dimension(width + 40,height + 50) + isVisible = false + } + + fun open(){ + isVisible = true + } + fun updatePercentages(model: DefaultTableModel){ + for(i in 0 until model.rowCount){ + model.setValueAt(getPercentage(table[i].weight,table),i,5) + } + } + + fun getPercentage(weight: Double, table: WeightBasedTable): String{ + return "${round((weight / table.totalWeight) * 100.0,2)}%" + } + fun round(value: Double, places: Int): Double { + require(places >= 0) + var bd: BigDecimal = BigDecimal.valueOf(value) + bd = bd.setScale(places, RoundingMode.HALF_UP) + return bd.toDouble() + } + } + + fun open(){ + for(item in table.alwaysTable){ + defaultModel.addRow(arrayOf(item.id,TableData.getItemName(item.id),item.minAmt,item.maxAmt)) + } + for(item in table.charmTable){ + charmModel.addRow(arrayOf(item.id,TableData.getItemName(item.id),item.minAmt,item.maxAmt,item.weight,getPercentage(item.weight,table.charmTable))) + } + for(item in table){ + mainModel.addRow(arrayOf(item.id,TableData.getItemName(item.id),item.minAmt,item.maxAmt,item.weight,getPercentage(item.weight,table))) + } + isVisible = true + } + + fun getPercentage(weight: Double, table: WeightBasedTable): String{ + return "${round((weight / table.totalWeight) * 100.0,2)}%" + } + + fun updateCharmPercentages(){ + for(i in 0 until charmModel.rowCount){ + charmModel.setValueAt(getPercentage(table.charmTable[i].weight,table.charmTable),i,5) + } + } + + fun updateMainPercentages(){ + for(i in 0 until mainModel.rowCount){ + mainModel.setValueAt(getPercentage(table[i].weight,table),i,5) + } + } + + fun round(value: Double, places: Int): Double { + require(places >= 0) + var bd: BigDecimal = BigDecimal.valueOf(value) + bd = bd.setScale(places, RoundingMode.HALF_UP) + return bd.toDouble() + } +} \ No newline at end of file diff --git a/src/main/kotlin/EditorConstants.kt b/src/main/kotlin/EditorConstants.kt new file mode 100644 index 0000000..15272c6 --- /dev/null +++ b/src/main/kotlin/EditorConstants.kt @@ -0,0 +1,21 @@ +import com.github.weisj.darklaf.LafManager +import com.github.weisj.darklaf.theme.OneDarkTheme + +object EditorConstants { + var BUILD_NUMBER = "0.1.0" + + var DARK_MODE = true + + var CONFIG_PATH = "" + + var VALID_FILES = arrayOf("drop_tables.json","npc_configs.json","item_configs.json", "shops.json", "object_configs.json") + + var CREDITS = arrayOf( + "weisJ - darklaf look and feel themes http://github.com/weisj", + "ceikry - design and programming http://gitlab.com/ceikry" + ) + + fun updateTheme(){ + if(EditorConstants.DARK_MODE) LafManager.install(OneDarkTheme()) else LafManager.install() + } +} \ No newline at end of file diff --git a/src/main/kotlin/EditorData.kt b/src/main/kotlin/EditorData.kt new file mode 100644 index 0000000..d5f0c35 --- /dev/null +++ b/src/main/kotlin/EditorData.kt @@ -0,0 +1,19 @@ +import org.json.simple.JSONArray +import org.json.simple.parser.JSONParser +import java.io.File +import java.io.FileReader + +open class EditorData(val fileName: String) { + var reader: FileReader? = null// = FileReader(EditorConstants.CONFIG_PATH + File.separator + fileName) + val parser = JSONParser() + var data = JSONArray() + + fun configureParser(){ + reader = FileReader(EditorConstants.CONFIG_PATH + File.separator + fileName) + data = parser.parse(reader) as JSONArray + } + + open fun parse() {} + open fun save() {} + open fun show() {} +} \ No newline at end of file diff --git a/src/main/kotlin/Editors.kt b/src/main/kotlin/Editors.kt new file mode 100644 index 0000000..3209b90 --- /dev/null +++ b/src/main/kotlin/Editors.kt @@ -0,0 +1,269 @@ +import org.json.simple.JSONArray +import org.json.simple.JSONObject +import org.json.simple.parser.JSONParser +import java.io.File +import java.io.FileReader +import java.io.FileWriter +import java.io.IOException +import java.lang.Exception +import java.lang.StringBuilder +import javax.script.ScriptEngineManager + +enum class Editors(val data: EditorData) { + DROP_TABLES(object : EditorData("drop_tables.json"){ + var counter = 0 + override fun parse() { + Logger.logInfo("Parsing Drop Tables") + configureParser() + data.forEach { dropTableRaw -> + val dropTable = dropTableRaw as org.json.simple.JSONObject + val table = NPCDropTable() + table.ids = dropTable["ids"].toString() + parseTable(dropTable["main"] as JSONArray,table,false) + parseTable(dropTable["default"] as JSONArray,table,true) + parseTable(dropTable["charm"] as JSONArray,table,false) + table.description = (dropTable["description"] ?: "").toString() + TableData.tables.add(table) + counter++ + } + Logger.logInfo("$counter Drop Tables Parsed.") + } + + override fun save() { + val CHARMS = intArrayOf(12160,12163,12159,12158) + val dtables = JSONArray() + for(table in TableData.tables){ + val t = JSONObject() + t.put("ids",table.ids) + val main = saveTable(table) + val charms = saveTable(table.charmTable) + val always = saveTable(table.alwaysTable) + t.put("main",main) + t.put("charm",charms) + t.put("default",always) + t.put("description",table.description) + dtables.add(t) + } + + + val manager = ScriptEngineManager() + val scriptEngine = manager.getEngineByName("JavaScript") + scriptEngine.put("jsonString", dtables.toJSONString()) + scriptEngine.eval("result = JSON.stringify(JSON.parse(jsonString), null, 2)") + val prettyPrintedJson = scriptEngine["result"] as String + + try { + FileWriter(EditorConstants.CONFIG_PATH + File.separator + fileName).use { file -> + file.write(prettyPrintedJson) + file.flush() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + + override fun show() { + super.show() + } + }), + NPC_CONFIGS(object : EditorData("npc_configs.json"){ + override fun parse() { + Logger.logInfo("Parsing NPC configs") + configureParser() + data.forEach { npcDataRaw -> + val npcData = npcDataRaw as JSONObject + val id = npcData["id"].toString().toInt() + val name = npcData["name"].toString() + TableData.npcNames[id] = name + TableData.npcConfigKeys.addAll(npcData.keys.toHashSet() as HashSet) + TableData.npcConfigs.add(npcData) + } + } + + override fun save() { + val array = JSONArray() + TableData.npcConfigs.forEach { + array.add(it) + } + val manager = ScriptEngineManager() + val scriptEngine = manager.getEngineByName("JavaScript") + scriptEngine.put("jsonString", array.toJSONString()) + scriptEngine.eval("result = JSON.stringify(JSON.parse(jsonString), null, 2)") + val prettyPrintedJson = scriptEngine["result"] as String + + try { + FileWriter(EditorConstants.CONFIG_PATH + File.separator + fileName).use { file -> + file.write(prettyPrintedJson) + file.flush() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + }), + ITEM_CONFIGS(object : EditorData("item_configs.json"){ + override fun parse() { + configureParser() + data.forEach { itemDataRaw -> + val itemData = itemDataRaw as JSONObject + val id = itemData["id"].toString().toInt() + val name = itemData["name"].toString() + TableData.itemConfigKeys.addAll(itemData.keys.toHashSet() as HashSet) + TableData.itemNames[id] = name + TableData.itemConfigs.add(itemData) + } + } + + override fun save() { + val array = JSONArray() + TableData.itemConfigs.forEach { + array.add(it) + } + val manager = ScriptEngineManager() + val scriptEngine = manager.getEngineByName("JavaScript") + scriptEngine.put("jsonString", array.toJSONString()) + scriptEngine.eval("result = JSON.stringify(JSON.parse(jsonString), null, 2)") + val prettyPrintedJson = scriptEngine["result"] as String + + try { + FileWriter(EditorConstants.CONFIG_PATH + File.separator + fileName).use { file -> + file.write(prettyPrintedJson) + file.flush() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + }), + OBJECT_CONFIGS(object : EditorData("object_configs.json"){ + override fun parse() { + Logger.logInfo("Parsing Object configs") + configureParser() + data.forEach { ObjDataRaw -> + val objData = ObjDataRaw as JSONObject + TableData.objConfigKeys.addAll(objData.keys.toHashSet() as HashSet) + TableData.objConfigs.add(objData) + } + } + + override fun save() { + val array = JSONArray() + TableData.objConfigs.forEach { + array.add(it) + } + val manager = ScriptEngineManager() + val scriptEngine = manager.getEngineByName("JavaScript") + scriptEngine.put("jsonString", array.toJSONString()) + scriptEngine.eval("result = JSON.stringify(JSON.parse(jsonString), null, 2)") + val prettyPrintedJson = scriptEngine["result"] as String + + try { + FileWriter(EditorConstants.CONFIG_PATH + File.separator + fileName).use { file -> + file.write(prettyPrintedJson) + file.flush() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + }), + SHOPS(object : EditorData("shops.json"){ + override fun parse() { + Logger.logInfo("Parsing shop data...") + configureParser() + var counter = 0 + + data.forEach { shopDataRaw -> + val shopData = shopDataRaw as JSONObject + val id = shopData["id"].toString().toInt() + val title = shopData["title"].toString() + val general = shopData["general_store"].toString().toBoolean() + val stock = parseStock(shopData["stock"].toString()) + val npcs = if(shopData["npcs"].toString().isNotBlank()) shopData["npcs"].toString() else "" + val currency = shopData["currency"].toString().toInt() + val highAlch = shopData["high_alch"].toString() == "1" + TableData.shops[id] = TableData.Shop(id,title,stock,npcs,currency,general,highAlch) + counter++ + } + + Logger.logInfo("Loaded $counter shops.") + } + + override fun save() { + val shs = JSONArray() + + fun ArrayList.isLast(thing: T): Boolean{ + return indexOf(thing) == size - 1 + } + + for((_,shop) in TableData.shops){ + val sh = JSONObject() + sh.put("id",shop.id.toString()) + sh.put("title",shop.title) + val stockBuilder = StringBuilder() + for(item in shop.stock){ + stockBuilder.append("{${item.id},${if(item.infinite) "inf" else item.amount}}${if(shop.stock.isLast(item)) "" else "-"}") + } + sh.put("stock",stockBuilder.toString()) + sh.put("npcs",shop.npcs) + sh.put("currency",shop.currency.toString()) + sh.put("high_alch",if(shop.high_alch) "1" else "0") + sh.put("general_store",shop.general_store.toString()) + shs.add(sh) + } + + + val manager = ScriptEngineManager() + val scriptEngine = manager.getEngineByName("JavaScript") + scriptEngine.put("jsonString", shs.toJSONString()) + scriptEngine.eval("result = JSON.stringify(JSON.parse(jsonString), null, 2)") + val prettyPrintedJson = scriptEngine["result"] as String + + try { + FileWriter(EditorConstants.CONFIG_PATH + File.separator + fileName).use { file -> + file.write(prettyPrintedJson) + file.flush() + } + } catch (e: IOException) { + e.printStackTrace() + } + } + }) +} + + +fun parseStock(stock: String): ArrayList{ + val items = ArrayList() + if(stock.isEmpty()){ + return items + } + stock.split('-').map { + val tokens = it.replace("{", "").replace("}", "").split(",".toRegex()).toTypedArray() + var amount = tokens[1].trim() + items.add(Item(tokens[0].trim().toInt(),amount,amount == "inf")) + } + return items +} +private fun parseTable(data: JSONArray, destTable: WeightBasedTable, isAlways: Boolean) { + for(it in data){ + val item = it as JSONObject + val id = item["id"].toString().toInt() + val minAmount = item["minAmount"].toString().toInt() + val maxAmount = item["maxAmount"].toString().toInt() + val weight = item["weight"].toString().toDouble() + val newItem = WeightedItem(id,minAmount.toString(),maxAmount.toString(),weight.toDouble(),isAlways) + destTable.add(newItem) + } +} +private fun saveTable(table: WeightBasedTable): JSONArray{ + val arr = JSONArray() + for(item in table){ + val it = JSONObject() + it.put("id",item.id.toString()) + it.put("weight",item.weight.toString()) + it.put("minAmount",item.minAmt.toString()) + it.put("maxAmount",item.maxAmt.toString()) + arr.add(it) + } + return arr +} diff --git a/src/main/kotlin/FileLoader.kt b/src/main/kotlin/FileLoader.kt new file mode 100644 index 0000000..65ad24c --- /dev/null +++ b/src/main/kotlin/FileLoader.kt @@ -0,0 +1,106 @@ +import org.json.simple.JSONArray +import org.json.simple.JSONObject +import org.json.simple.parser.JSONParser +import java.io.FileReader +import java.lang.Exception +import javax.swing.* + +object FileLoader{ + val loadStatus = JLabel("File not loaded.") + val loadButton = JButton("Select drop_tables.json") + val chooser = JFileChooser() + var dropTableList: TableList? = null + + private fun parseTable(data: JSONArray, destTable: WeightBasedTable, isAlways: Boolean) { + for(it in data){ + val item = it as JSONObject + val id = item["id"].toString().toInt() + val minAmount = item["minAmount"].toString().toInt() + val maxAmount = item["maxAmount"].toString().toInt() + val weight = item["weight"].toString().toDouble() + val newItem = WeightedItem(id,minAmount.toString(),maxAmount.toString(),weight.toDouble(),isAlways) + destTable.add(newItem) + } + } + + fun load(){ + Logger.logInfo("Parsing Drop Tables...") + var parser = JSONParser() + var reader = FileReader(EditorConstants.CONFIG_PATH + "/drop_tables.json") + var data = parser.parse(reader) as JSONArray + var counter = 0 + + data.forEach { dropTableRaw -> + val dropTable = dropTableRaw as JSONObject + val table = NPCDropTable() + table.ids = dropTable["ids"].toString() + parseTable(dropTable["main"] as JSONArray,table,false) + parseTable(dropTable["default"] as JSONArray,table,true) + parseTable(dropTable["charm"] as JSONArray,table,false) + table.description = (dropTable["description"] ?: "").toString() + TableData.tables.add(table) + counter++ + } + Logger.logInfo("Parsed Drop Tables.") + Logger.logInfo("Loaded $counter drop tables.") + + Logger.logInfo("Parsing Item Configs...") + try { + data = parser.parse(FileReader(EditorConstants.CONFIG_PATH + "/item_configs.json")) as JSONArray + data.forEach { itemDataRaw -> + val itemData = itemDataRaw as JSONObject + val id = itemData["id"].toString().toInt() + val name = itemData["name"].toString() + TableData.itemNames[id] = name + } + Logger.logInfo("Parsed item configs.") + } catch (e: Exception){ + Logger.logErr("Failed to load item_configs.json. Item names will be unavailable when viewing and editing shops.") + } + Logger.logInfo("Parsing NPC configs...") + try { + data = parser.parse(FileReader(EditorConstants.CONFIG_PATH + "/npc_configs.json")) as JSONArray + data.forEach { npcDataRaw -> + val npcData = npcDataRaw as JSONObject + val id = npcData["id"].toString().toInt() + val name = npcData["name"].toString() + TableData.npcNames[id] = name + } + Logger.logInfo("Parsed NPC configs") + } catch (e: Exception){ + Logger.logErr("Failed to load npc_configs.json. Drop tables will lack proper naming. Good luck figuring out what you want to edit.") + } + + } +} + +class Item(var id: Int,var amount: String,var infinite: Boolean) +class WeightedItem(var id: Int, var minAmt: String, var maxAmt: String, var weight: Double, var isAlways: Boolean) + +private val CHARMS = intArrayOf(12160,12163,12159,12158) + +open class WeightBasedTable() : ArrayList(){ + var totalWeight: Double = 0.0 + override fun add(element: WeightedItem): Boolean { + totalWeight += element.weight + return super.add(element) + } +} + +class NPCDropTable() : WeightBasedTable(){ + val charmTable = WeightBasedTable() + val alwaysTable = WeightBasedTable() + var ids = "" + var description = "" + override fun add(element: WeightedItem): Boolean { + if(element.isAlways){ + alwaysTable.add(element) + return true + } + if(CHARMS.contains(element.id)){ + charmTable.add(element) + return true + } + return super.add(element) + } +} \ No newline at end of file diff --git a/src/main/kotlin/Globals.kt b/src/main/kotlin/Globals.kt new file mode 100644 index 0000000..0fe17ef --- /dev/null +++ b/src/main/kotlin/Globals.kt @@ -0,0 +1,57 @@ +import java.awt.BorderLayout +import java.awt.Dimension +import javax.swing.* + +val confirmation = object : JFrame("Notice"){ + init { + layout = BorderLayout() + minimumSize = Dimension(250,100) + defaultCloseOperation = HIDE_ON_CLOSE + val okButton = JButton("OK") + okButton.addActionListener { + isVisible = false + } + add(JLabel("Added a new row to the table."), BorderLayout.NORTH) + add(okButton, BorderLayout.SOUTH) + setLocationRelativeTo(null) + isVisible = false + pack() + } +} +val saveConfirmation = object : JFrame("Notice"){ + init { + minimumSize = Dimension(250,100) + defaultCloseOperation = HIDE_ON_CLOSE + layout = BorderLayout() + val okButton = JButton("OK") + okButton.addActionListener { + isVisible = false + } + add(JLabel("Data saved."), BorderLayout.NORTH) + add(okButton, BorderLayout.SOUTH) + isVisible = false + setLocationRelativeTo(null) + pack() + } +} + +val credits = object : JFrame("Credits"){ + init { + defaultCloseOperation = HIDE_ON_CLOSE + val creditPanel = JPanel() + creditPanel.layout = BoxLayout(creditPanel, BoxLayout.PAGE_AXIS) + layout = BorderLayout() + + for(credit in EditorConstants.CREDITS){ + creditPanel.add(JLabel(credit)) + } + + add(creditPanel) + + isVisible = false + + setLocationRelativeTo(null) + + pack() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ItemMenu.kt b/src/main/kotlin/ItemMenu.kt new file mode 100644 index 0000000..773e1fe --- /dev/null +++ b/src/main/kotlin/ItemMenu.kt @@ -0,0 +1,100 @@ +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.KeyEvent +import java.awt.event.KeyListener +import java.awt.event.MouseEvent +import java.awt.event.MouseListener +import java.util.* +import java.util.regex.PatternSyntaxException +import javax.swing.* +import javax.swing.table.DefaultTableCellRenderer +import javax.swing.table.DefaultTableModel +import javax.swing.table.TableRowSorter + +object ItemMenu : JFrame("Item Selection Menu") { + var model = DefaultTableModel() + var searchField = JTextField() + var amountField = JTextField() + var sorter = TableRowSorter(model) + var cellRenderer = DefaultTableCellRenderer() + var populated = false + var caller: ((Int,String) -> Unit)? = null + private fun filter() { + var rf: RowFilter? = null + //If current expression doesn't parse, don't update. + rf = try { + RowFilter.regexFilter(searchField.text, 1) + } catch (e: PatternSyntaxException) { + return + } + sorter.rowFilter = rf + } + + fun open() { + if (!populated) { + SwingUtilities.invokeLater { + for ((id,_) in TableData.itemNames) { + model.addRow(arrayOf(id,TableData.getItemName(id))) + } + isVisible = true + populated = true + } + } else { + isVisible = true + } + } + + init { + layout = BorderLayout() + cellRenderer.toolTipText = "Double-Click to select." + val searchPanel = JPanel() + val searchLabel = JLabel("Search for Item:") + val amountLabel = JLabel("Amount: ") + amountField.preferredSize = Dimension(45, 20) + searchField.preferredSize = Dimension(100, 20) + searchPanel.add(searchLabel) + searchPanel.add(searchField) + add(searchPanel, BorderLayout.NORTH) + setLocationRelativeTo(null) + val itemTable: JTable = object : JTable(model) { + override fun editCellAt(i: Int, i1: Int, eventObject: EventObject): Boolean { + return false + } + } + model.addColumn("ID") + model.addColumn("Name") + itemTable.rowSorter = sorter + itemTable.columnModel.getColumn(0).maxWidth = 55 + itemTable.columnModel.getColumn(0).cellRenderer = cellRenderer + itemTable.columnModel.getColumn(1).cellRenderer = cellRenderer + itemTable.addMouseListener(object : MouseListener { + override fun mouseClicked(mouseEvent: MouseEvent) { + if (mouseEvent.clickCount == 2) { + val table = mouseEvent.source as JTable + val row = table.selectedRow + val selectedID = table.getValueAt(row,0).toString().toInt() + val selectedName = table.getValueAt(row,1).toString() + caller?.invoke(selectedID,selectedName) + isVisible = false + } + } + + override fun mousePressed(mouseEvent: MouseEvent) {} + override fun mouseReleased(mouseEvent: MouseEvent) {} + override fun mouseEntered(mouseEvent: MouseEvent) {} + override fun mouseExited(mouseEvent: MouseEvent) {} + }) + searchField.addKeyListener(object : KeyListener { + override fun keyTyped(keyEvent: KeyEvent) { + filter() + } + + override fun keyPressed(keyEvent: KeyEvent) {} + override fun keyReleased(keyEvent: KeyEvent) {} + }) + val scrollPane = JScrollPane(itemTable) + add(scrollPane, BorderLayout.SOUTH) + pack() + isVisible = false + } +} \ No newline at end of file diff --git a/src/main/kotlin/Logger.kt b/src/main/kotlin/Logger.kt new file mode 100644 index 0000000..dc024e4 --- /dev/null +++ b/src/main/kotlin/Logger.kt @@ -0,0 +1,21 @@ +import com.github.ajalt.mordant.rendering.TextColors +import com.github.ajalt.mordant.terminal.Terminal +import java.text.SimpleDateFormat +import java.util.* + +object Logger { + val t = Terminal() + val formatter = SimpleDateFormat("HH:mm:ss") + + fun logInfo(message: String){ + t.println("${getTime()} ${TextColors.white("[INFO] $message")}") + } + + fun logErr(message: String){ + t.println("${getTime()} ${TextColors.red("[ ERR] $message")}") + } + + fun getTime(): String{ + return "[" + formatter.format(Date(System.currentTimeMillis())) +"]" + } +} \ No newline at end of file diff --git a/src/main/kotlin/MainScreen.kt b/src/main/kotlin/MainScreen.kt new file mode 100644 index 0000000..4ec31fc --- /dev/null +++ b/src/main/kotlin/MainScreen.kt @@ -0,0 +1,143 @@ +import com.github.weisj.darklaf.LafManager +import com.github.weisj.darklaf.theme.DarculaTheme +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.FlowLayout +import java.io.File +import java.util.* +import javax.swing.* +import javax.swing.table.DefaultTableModel + +object MainScreen : JFrame("RS09 Thanos Tool") { + val dirChooser = object : JFileChooser(){ + override fun updateUI() { + EditorConstants.updateTheme() + super.updateUI() + } + } + val selectDir = JButton("Select Config Folder") + val openDrops = JButton("Edit Drop Tables") + val openNPC = JButton("Edit NPC Configs") + val openItem = JButton("Edit Item Configs") + val openShops = JButton("Edit Shops") + val openObjects = JButton("Edit Object Configs") + var loadedModel = object : DefaultTableModel(){} + val loadedTable = object : JTable(loadedModel){ + override fun editCellAt(p0: Int, p1: Int): Boolean { + return false + } + + override fun editCellAt(p0: Int, p1: Int, p2: EventObject?): Boolean { + return false + } + } + val pane = JScrollPane(loadedTable) + + init { + isResizable = false + layout = BorderLayout() + dirChooser.fileSelectionMode = JFileChooser.DIRECTORIES_ONLY + defaultCloseOperation = EXIT_ON_CLOSE + preferredSize = Dimension(550,300) + loadedModel.addColumn("Filename") + + iconImage = ImageIcon(javaClass.getResource("/thanos.png")).image + + + selectDir.addActionListener { + val response = dirChooser.showOpenDialog(null) + EditorConstants.updateTheme() + if(response == JFileChooser.APPROVE_OPTION){ + Logger.logInfo("Selected directory: ${dirChooser.selectedFile.absolutePath}") + EditorConstants.CONFIG_PATH = dirChooser.selectedFile.absolutePath + showLoaded() + } + } + + openDrops.addActionListener { + FileLoader.dropTableList = TableList() + FileLoader.dropTableList!!.open() + } + + openItem.addActionListener { + TableEditor(Editors.ITEM_CONFIGS).open() + } + + openNPC.addActionListener { + TableEditor(Editors.NPC_CONFIGS).open() + } + + openObjects.addActionListener { + TableEditor(Editors.OBJECT_CONFIGS).open() + } + + openShops.addActionListener { + ShopList().open() + } + + val showCredits = JButton("Show Credits") + showCredits.addActionListener { + credits.isVisible = true + } + + val editorPanel = JPanel() + val selectionPanel = JPanel() + + editorPanel.layout = BoxLayout(editorPanel, BoxLayout.Y_AXIS)//BoxLayout(editorPanel, BoxLayout.PAGE_AXIS) + for(i in arrayOf(openDrops, openNPC, openItem, openObjects, openShops, showCredits)){ + i.preferredSize = Dimension(175,40) + i.maximumSize = Dimension(175,40) + i.isEnabled = i == showCredits + editorPanel.add(i) + } + + add(editorPanel, BorderLayout.WEST) + + val buttonPane = JPanel() + buttonPane.layout = BorderLayout() + selectDir.preferredSize = Dimension(300,40) + selectDir.maximumSize = Dimension(300,40) + buttonPane.add(selectDir, BorderLayout.CENTER) + + selectionPanel.layout = BoxLayout(selectionPanel, BoxLayout.Y_AXIS) + selectionPanel.add(buttonPane) + + pane.preferredSize = Dimension(225,200) + selectionPanel.add(pane) + + add(JSeparator(SwingConstants.VERTICAL), BorderLayout.CENTER) + + setLocationRelativeTo(null) + add(selectionPanel, BorderLayout.EAST) + EditorConstants.updateTheme() + pack() + } +} + +fun showLoaded(){ + MainScreen.loadedModel = object : DefaultTableModel(){} + MainScreen.loadedModel.addColumn("Filename") + MainScreen.loadedTable.model = MainScreen.loadedModel + + val files = File(EditorConstants.CONFIG_PATH).listFiles() ?: return + + for(file in files){ + if(file.name in EditorConstants.VALID_FILES){ + MainScreen.loadedModel.addRow(arrayOf(file.name)) + when(file.nameWithoutExtension){ + "drop_tables" -> MainScreen.openDrops.isEnabled = true.also { Editors.DROP_TABLES.data.parse() } + "npc_configs" -> MainScreen.openNPC.isEnabled = true.also { Editors.NPC_CONFIGS.data.parse() } + "item_configs" -> MainScreen.openItem.isEnabled = true.also { Editors.ITEM_CONFIGS.data.parse() } + "shops" -> MainScreen.openShops.isEnabled = true.also { Editors.SHOPS.data.parse() } + "object_configs" -> MainScreen.openObjects.isEnabled = true.also { Editors.OBJECT_CONFIGS.data.parse()} + } + } + } + + MainScreen.pane.revalidate() + MainScreen.pane.repaint() +} + +fun main() { + MainScreen.isVisible = true +} \ No newline at end of file diff --git a/src/main/kotlin/ShopEdit.kt b/src/main/kotlin/ShopEdit.kt new file mode 100644 index 0000000..d0ab1bd --- /dev/null +++ b/src/main/kotlin/ShopEdit.kt @@ -0,0 +1,170 @@ +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.* +import java.util.* +import javax.swing.* +import javax.swing.event.ChangeEvent +import javax.swing.table.DefaultTableModel +import kotlin.collections.ArrayList + +class ShopEdit(val shopID: Int) : JFrame("Edit Shop") { + val shop = TableData.shops[shopID] + val idField = JTextField() + val titleField = JTextField() + val currencyField = JTextField() + val generalCheckbox = JCheckBox("General Store") + val highAlchCheckbox = JCheckBox("High Alch Prices") + var model = DefaultTableModel() + + + init { + layout = BorderLayout() + val topPanel = JPanel() + val idLabel = JLabel("ID: ") + idField.preferredSize = Dimension(40, 20) + idField.text = shopID.toString() + idField.addKeyListener(object : KeyListener{ + override fun keyTyped(e: KeyEvent?) {} + override fun keyPressed(e: KeyEvent?) {} + override fun keyReleased(e: KeyEvent?) { + shop?.id = idField.text.toInt() + } + }) + topPanel.add(idLabel) + topPanel.add(idField) + + val currencyLabel = JLabel("Currency: ") + currencyField.preferredSize = Dimension(50,20) + currencyField.text = shop?.currency.toString() + currencyField.addKeyListener(object : KeyListener{ + override fun keyTyped(e: KeyEvent?) {} + override fun keyPressed(e: KeyEvent?) {} + override fun keyReleased(e: KeyEvent?) { + shop?.currency = currencyField.text.toInt() + } + }) + topPanel.add(currencyLabel) + topPanel.add(currencyField) + + val titleLabel = JLabel("Title: ") + titleField.preferredSize = Dimension(300,20) + titleField.text = shop?.title + titleField.addKeyListener(object : KeyListener{ + override fun keyTyped(e: KeyEvent?) {} + override fun keyPressed(e: KeyEvent?) {} + override fun keyReleased(e: KeyEvent?) { + shop?.title = titleField.text + } + }) + topPanel.add(titleLabel) + topPanel.add(titleField) + + add(topPanel,BorderLayout.NORTH) + + val checkPanel = JPanel() + val npcLabel = JLabel("NPCs: ") + val npcField = JTextField() + npcField.addKeyListener(object : KeyListener{ + override fun keyTyped(e: KeyEvent?) { + shop?.npcs = npcField.text + e?.keyChar + } + override fun keyPressed(e: KeyEvent?) {} + override fun keyReleased(e: KeyEvent?) {} + }) + npcField.preferredSize = Dimension(50,20) + npcField.text = shop?.npcs + checkPanel.add(npcLabel) + checkPanel.add(npcField) + generalCheckbox.isSelected = shop?.general_store ?: false + generalCheckbox.addChangeListener { + shop!!.general_store = !shop.general_store + } + checkPanel.add(generalCheckbox) + highAlchCheckbox.isBorderPaintedFlat = shop?.high_alch ?: false + highAlchCheckbox.isSelected = shop?.high_alch ?: false + highAlchCheckbox.addChangeListener { + shop!!.high_alch = !shop.high_alch + } + checkPanel.add(highAlchCheckbox) + + val addButton = JButton("Add Item") + addButton.addActionListener { + ItemMenu.caller = {id,name -> + model.addRow(arrayOf(name,id,0,false)) + shop!!.stock.add(Item(id,"0",false)) + } + ItemMenu.open() + } + checkPanel.add(addButton) + add(checkPanel,BorderLayout.CENTER) + + val itemTable: JTable = object : JTable(model) { + override fun editCellAt(row: Int, column: Int): Boolean { + if(column == 0 || column == 1){ + ItemMenu.caller = {id,name -> + model.setValueAt(name,row,0) + model.setValueAt(id,row,1) + shop!!.stock[row].id = id + } + ItemMenu.open() + return false + } + return super.editCellAt(row, column) + } + + override fun editCellAt(row: Int, column: Int, e: EventObject?): Boolean { + if(column == 0 || column == 1){ + ItemMenu.caller = {id,name -> + model.setValueAt(name,row,0) + model.setValueAt(id,row,1) + shop!!.stock[row].id = id + } + ItemMenu.open() + return false + } + return super.editCellAt(row, column, e) + } + override fun editingStopped(e: ChangeEvent?) { + val editor = cellEditor + val newValue = editor.cellEditorValue.toString() + when(editingColumn){ + 1 -> TableData.shops[shopID]!!.stock[editingRow].id = newValue.toInt() + 2 -> TableData.shops[shopID]!!.stock[editingRow].amount = newValue + 3 -> TableData.shops[shopID]!!.stock[editingRow].infinite = newValue.toBoolean() + } + super.editingStopped(e) + } + } + itemTable.putClientProperty("terminateEditOnFocusLost", true); + + itemTable.inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete") + itemTable.actionMap.put("delete",object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + try { + shop!!.stock.removeAt(itemTable.selectedRow) + model.removeRow(itemTable.selectedRow) + } catch(e: Exception){ + Logger.logErr("Tried to remove nonexistent row ${itemTable.selectedRow}") + } + } + }) + + model.addColumn("Name") + model.addColumn("ID") + model.addColumn("Amount") + model.addColumn("Infinite?") + + val scrollPane = JScrollPane(itemTable) + add(scrollPane,BorderLayout.SOUTH) + + pack() + isVisible = false + } + + fun open(){ + for(item in shop?.stock ?: ArrayList()){ + model.addRow(arrayOf(TableData.itemNames[item.id] ?: "unknown",item.id,item.amount,item.infinite)) + } + isVisible = true + } +} \ No newline at end of file diff --git a/src/main/kotlin/ShopList.kt b/src/main/kotlin/ShopList.kt new file mode 100644 index 0000000..c47d726 --- /dev/null +++ b/src/main/kotlin/ShopList.kt @@ -0,0 +1,131 @@ +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.* +import java.util.* +import java.util.regex.PatternSyntaxException +import javax.swing.* +import javax.swing.table.DefaultTableCellRenderer +import javax.swing.table.DefaultTableModel +import javax.swing.table.TableRowSorter +import kotlin.collections.ArrayList + +class ShopList : JFrame("RS09 Shop Editor") { + val searchField = JTextField() + var model = DefaultTableModel() + var sorter = TableRowSorter(model) + var cellRenderer = DefaultTableCellRenderer() + var populated = false + + init { + isVisible = false + layout = BorderLayout() + + val searchPanel = JPanel() + val searchLabel = JLabel("Search for shop: ") + val addButton = JButton("Add Shop") + val saveButton = JButton("Save Shops") + addButton.addActionListener { + val highestID = TableData.shops.keys.toIntArray()[TableData.shops.size - 1] + TableData.shops.put(highestID + 1, TableData.Shop(highestID + 1,"", ArrayList(),"",995,false,false)) + ShopEdit(highestID + 1).open() + } + saveButton.addActionListener { + Editors.SHOPS.data.save() + saveConfirmation.isVisible = true + } + searchField.preferredSize = Dimension(100, 20) + searchPanel.add(searchLabel) + searchPanel.add(searchField) + searchPanel.add(addButton) + searchPanel.add(saveButton) + add(searchPanel, BorderLayout.NORTH) + setLocationRelativeTo(null) + + val itemTable: JTable = object : JTable(model) { + override fun editCellAt(i: Int, i1: Int, eventObject: EventObject): Boolean { + return false + } + } + + itemTable.inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete") + itemTable.actionMap.put("delete",object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + try { + val table = e?.source as JTable + TableData.shops.remove(table.getValueAt(table.selectedRow,0)) + model.removeRow(table.convertRowIndexToModel(table.selectedRow)) + } catch(e: Exception){ + Logger.logErr("Tried to remove nonexistent row ${itemTable.selectedRow}") + } + } + }) + + model.addColumn("ID") + model.addColumn("Name") + + itemTable.rowSorter = sorter + itemTable.columnModel.getColumn(0).maxWidth = 55 + itemTable.columnModel.getColumn(0).cellRenderer = cellRenderer + itemTable.columnModel.getColumn(1).cellRenderer = cellRenderer + + itemTable.addMouseListener(object : MouseListener { + override fun mouseClicked(mouseEvent: MouseEvent) { + if (mouseEvent.clickCount == 2) { + val table = mouseEvent.source as JTable + val row = table.selectedRow + ShopEdit(table.getValueAt(row,0) as Int).open() + } + } + + override fun mousePressed(mouseEvent: MouseEvent) {} + override fun mouseReleased(mouseEvent: MouseEvent) {} + override fun mouseEntered(mouseEvent: MouseEvent) {} + override fun mouseExited(mouseEvent: MouseEvent) {} + }) + + searchField.addKeyListener(object : KeyListener { + override fun keyTyped(keyEvent: KeyEvent) { + filter() + } + + override fun keyPressed(keyEvent: KeyEvent) {} + override fun keyReleased(keyEvent: KeyEvent) {} + }) + + + val scrollPane = JScrollPane(itemTable) + add(scrollPane, BorderLayout.SOUTH) + + pack() + isVisible = false + } + + private fun filter() { + var rf: RowFilter? = null + //If current expression doesn't parse, don't update. + rf = try { + RowFilter.regexFilter(searchField.text, 1) + } catch (e: PatternSyntaxException) { + return + } + sorter.rowFilter = rf + } + + fun open() { + if (!populated) { + Logger.logInfo("Loading shops data...") + for(i in 0 until model.rowCount){ + model.removeRow(0) + } + SwingUtilities.invokeLater { + for ((id,shop) in TableData.shops) { + model.addRow(arrayOf(id, shop.title)) + } + isVisible = true + populated = true + } + } else { + isVisible = true + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/TableData.kt b/src/main/kotlin/TableData.kt new file mode 100644 index 0000000..d5d6194 --- /dev/null +++ b/src/main/kotlin/TableData.kt @@ -0,0 +1,33 @@ +import org.json.simple.JSONArray +import org.json.simple.JSONObject +import java.io.File +import java.io.FileWriter +import java.io.IOException +import java.lang.StringBuilder +import javax.script.ScriptEngineManager + +object TableData { + class Shop(var id: Int,var title: String,var stock: ArrayList,var npcs: String,var currency: Int,var general_store: Boolean,var high_alch: Boolean) + val tables = ArrayList() + val itemNames = hashMapOf() + val npcNames = hashMapOf() + val npcConfigKeys = HashSet() + val npcConfigs = ArrayList() + val objConfigKeys = HashSet() + val objConfigs = ArrayList() + var itemConfigKeys = HashSet() + var itemConfigs = ArrayList() + val shops = hashMapOf() + + fun getItemName(id: Int): String{ + return when(id){ + 31 -> "RDT Slot" + 1 -> "Clue Scroll (easy)" + 5733 -> "Clue Scroll (med)" + 12070 -> "Clue Scroll (hard)" + 0 -> "Nothing" + else -> itemNames[id] ?: "Unknown" + } + } +} + diff --git a/src/main/kotlin/TableEditor.kt b/src/main/kotlin/TableEditor.kt new file mode 100644 index 0000000..60699be --- /dev/null +++ b/src/main/kotlin/TableEditor.kt @@ -0,0 +1,152 @@ +import org.json.simple.JSONArray +import org.json.simple.JSONObject +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.FlowLayout +import java.awt.event.ActionEvent +import java.awt.event.KeyEvent +import java.awt.event.KeyListener +import java.util.regex.PatternSyntaxException +import javax.swing.* +import javax.swing.event.TableModelEvent +import javax.swing.event.TableModelListener +import javax.swing.table.DefaultTableModel +import javax.swing.table.TableRowSorter + +/** + * Represents a generic JTable-based editor. Pretty much just a cleaner visualisation of JSON objects with editing functionality. + */ +class TableEditor(val editor: Editors) : JFrame("Editing ${editor.name.replace("_"," ").toLowerCase()}") { + val searchField = JTextField() + val model = DefaultTableModel() + var table = JTable(model) + val sorter = TableRowSorter(model) + + init { + layout = BorderLayout() + isVisible = false + table.autoResizeMode = JTable.AUTO_RESIZE_OFF + searchField.preferredSize = Dimension(200,20) + searchField.maximumSize = Dimension(200,20) + minimumSize = Dimension(1000, 650) + + searchField.addKeyListener(object : KeyListener{ + override fun keyTyped(p0: KeyEvent?) { filter() } + override fun keyPressed(p0: KeyEvent?) {} + override fun keyReleased(p0: KeyEvent?) {} + }) + + table.rowSorter = sorter + + + val addButton = JButton("Add Row") + val saveButton = JButton("Save Data") + + addButton.addActionListener { + getData().add(JSONObject()) + model.addRow(arrayOf()) + confirmation.isVisible = true + } + + saveButton.addActionListener { + editor.data.save() + saveConfirmation.isVisible = true + } + + val searchPanel = JPanel() + searchPanel.layout = FlowLayout() + searchPanel.add(JLabel("Search by Name ")) + searchPanel.add(searchField) + searchPanel.add(addButton) + searchPanel.add(saveButton) + + val pane = JScrollPane(table, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS) + add(pane, BorderLayout.CENTER) + add(searchPanel, BorderLayout.NORTH) + + val keys = when (editor) { + Editors.NPC_CONFIGS -> TableData.npcConfigKeys + Editors.ITEM_CONFIGS -> TableData.itemConfigKeys + Editors.OBJECT_CONFIGS -> TableData.objConfigKeys + else -> ArrayList() + } + + if(editor == Editors.OBJECT_CONFIGS){ + model.addColumn("ids") + model.addColumn("examine") + keys.remove("ids") + keys.remove("examine") + table.columnModel.getColumn(0).preferredWidth = 400 + table.columnModel.getColumn(1).preferredWidth = 600 + } else { + model.addColumn("id") + model.addColumn("name") + keys.remove("id") + keys.remove("name") + } + + keys.sortedBy { it.toString() }.forEach { model.addColumn(it) } + if(editor != Editors.OBJECT_CONFIGS) { + table.columnModel.getColumn(1).preferredWidth = 150 + } + + model.addTableModelListener(object : TableModelListener{ + override fun tableChanged(event: TableModelEvent?) { + event ?: return + if(event.type != TableModelEvent.UPDATE) return + val label = model.getColumnName(event.column) + getData()[event.firstRow][label] = model.getValueAt(event.firstRow, event.column) + Logger.logInfo("Changed ${getData()[event.firstRow]["name"]}'s $label to ${model.getValueAt(event.firstRow,event.column)}") + } + }) + + table.actionMap.put("delete",object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + try { + val table = e?.source as JTable + getData().removeAt(table.convertRowIndexToModel(table.selectedRow)) + model.removeRow(table.convertRowIndexToModel(table.selectedRow)) + } catch(e: Exception){ + Logger.logErr("Tried to remove nonexistent row ${table.selectedRow}") + } + } + }) + + EditorConstants.updateTheme() + pack() + } + + fun open(){ + val data = getData() + + for(datum in data){ + val vals = JSONArray() + for(i in 0 until model.columnCount){ + val label = model.getColumnName(i) + vals.add(datum[label] ?: "") + } + model.addRow(vals.toArray()) + } + isVisible = true + } + + private fun filter() { + var rf: RowFilter? = null + //If current expression doesn't parse, don't update. + rf = try { + RowFilter.regexFilter(searchField.text) + } catch (e: PatternSyntaxException) { + return + } + sorter.rowFilter = rf + } + + fun getData(): ArrayList { + return when(editor){ + Editors.ITEM_CONFIGS -> TableData.itemConfigs + Editors.NPC_CONFIGS -> TableData.npcConfigs + Editors.OBJECT_CONFIGS -> TableData.objConfigs + else -> ArrayList() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/TableList.kt b/src/main/kotlin/TableList.kt new file mode 100644 index 0000000..746ed3c --- /dev/null +++ b/src/main/kotlin/TableList.kt @@ -0,0 +1,126 @@ +import java.awt.BorderLayout +import java.awt.Dimension +import java.awt.event.* +import java.util.* +import java.util.regex.PatternSyntaxException +import javax.swing.* +import javax.swing.table.DefaultTableCellRenderer +import javax.swing.table.DefaultTableModel +import javax.swing.table.TableRowSorter + +class TableList : JFrame("RS09 Drop Table Editor") { + val searchField = JTextField() + var model = DefaultTableModel() + var sorter = TableRowSorter(model) + var cellRenderer = DefaultTableCellRenderer() + var populated = false + + init { + isVisible = false + layout = BorderLayout() + + val searchPanel = JPanel() + val searchLabel = JLabel("Search for table: ") + val addButton = JButton("Add Table") + val saveButton = JButton("Save Tables") + addButton.addActionListener { + TableData.tables.add(NPCDropTable()) + DropTableEditor(TableData.tables.last()).open() + } + saveButton.addActionListener { + Editors.DROP_TABLES.data.save() + saveConfirmation.isVisible = true + } + searchField.preferredSize = Dimension(100, 20) + searchPanel.add(searchLabel) + searchPanel.add(searchField) + searchPanel.add(addButton) + searchPanel.add(saveButton) + add(searchPanel, BorderLayout.NORTH) + setLocationRelativeTo(null) + + val itemTable: JTable = object : JTable(model) { + override fun editCellAt(i: Int, i1: Int, eventObject: EventObject): Boolean { + return false + } + } + + itemTable.inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), "delete") + itemTable.actionMap.put("delete",object : AbstractAction() { + override fun actionPerformed(e: ActionEvent?) { + try { + val table = e?.source as JTable + TableData.tables.removeAt(table.convertRowIndexToModel(table.selectedRow)) + model.removeRow(table.convertRowIndexToModel(table.selectedRow)) + } catch(e: Exception){ + Logger.logErr("Tried to remove nonexistent row ${itemTable.selectedRow}") + } + } + }) + + model.addColumn("Name") + + itemTable.rowSorter = sorter + itemTable.columnModel.getColumn(0).cellRenderer = cellRenderer + + itemTable.addMouseListener(object : MouseListener { + override fun mouseClicked(mouseEvent: MouseEvent) { + if (mouseEvent.clickCount == 2) { + val table = mouseEvent.source as JTable + val row = table.selectedRow + DropTableEditor(TableData.tables[table.convertRowIndexToModel(row)]).open() + } + } + + override fun mousePressed(mouseEvent: MouseEvent) {} + override fun mouseReleased(mouseEvent: MouseEvent) {} + override fun mouseEntered(mouseEvent: MouseEvent) {} + override fun mouseExited(mouseEvent: MouseEvent) {} + }) + + searchField.addKeyListener(object : KeyListener { + override fun keyTyped(keyEvent: KeyEvent) { + filter() + } + + override fun keyPressed(keyEvent: KeyEvent) {} + override fun keyReleased(keyEvent: KeyEvent) {} + }) + + + val scrollPane = JScrollPane(itemTable) + add(scrollPane, BorderLayout.SOUTH) + + pack() + isVisible = false + } + + private fun filter() { + var rf: RowFilter? = null + //If current expression doesn't parse, don't update. + rf = try { + RowFilter.regexFilter(searchField.text, 0) + } catch (e: PatternSyntaxException) { + return + } + sorter.rowFilter = rf + } + + fun open() { + if (!populated) { + Logger.logInfo("Loading table data...") + for(i in 0 until model.rowCount){ + model.removeRow(0) + } + SwingUtilities.invokeLater { + for (table in TableData.tables) { + model.addRow(arrayOf(if(table.ids.isNotBlank()) TableData.npcNames[table.ids.split(",")[0].toInt()] ?: "Unknown Table" else "None")) + } + isVisible = true + populated = true + } + } else { + isVisible = true + } + } +} \ No newline at end of file diff --git a/src/main/resources/thanos.png b/src/main/resources/thanos.png new file mode 100644 index 0000000000000000000000000000000000000000..bab94d64cfd6f32d91c6c3e9362ad6da3b1a4ba8 GIT binary patch literal 50080 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 JR?#-{e*w!qq7ncA literal 0 HcmV?d00001