diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..266514e --- /dev/null +++ b/build.gradle @@ -0,0 +1,32 @@ +group 'sneakyspook' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.1.51' + + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +apply plugin: 'kotlin' + +repositories { + mavenCentral() +} + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile 'no.tornado:tornadofx:1.7.11' + compile "com.github.salomonbrys.kotson:kotson:2.5.0" +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} \ No newline at end of file diff --git a/coursenames.json b/coursenames.json new file mode 100644 index 0000000..086542d --- /dev/null +++ b/coursenames.json @@ -0,0 +1,42 @@ +[ + "Simplicity is Best", + "Punch! Shoot! Smack!", + "Bringing Topknots Back", + "Aim True", + "Group Activity", + "Pattern Play", + "A Tale of Dietary Fiber", + "Monster Maw", + "Master of the Delayed Response", + "Game Gamble: Beginner", + "The Soul of Japan", + "All Singing, All Dancing", + "Full-Belly Dojo", + "Round-Object Fan Club", + "Factory Tourism", + "On the Job", + "Getting Vocal", + "Be a Good Sport", + "Extreme Sports", + "So Many Monkeys!", + "Monster Maw 2", + "Remix Medley", + "Super Remix Medley", + "That's Show Biz!", + "Game Gamble: Intermediate", + "All or Nothing!", + "Demon Slayer", + "Tales of Romance", + "Back and So Forth", + "Karate Joe vs. the Monster", + "Copycats", + "Group Activity 2", + "Lockstep Lockdown", + "Getting Vocal 2", + "Spaaaaaaaaaaaaaaace!", + "Rhythm Safari", + "Hello, Ladies...", + "Game Gamble: Advanced", + "Wario...Where?", + "Wario...Where? 2: The Sequel" +] \ No newline at end of file diff --git a/gamenames.json b/gamenames.json new file mode 100644 index 0000000..dec8cae --- /dev/null +++ b/gamenames.json @@ -0,0 +1,106 @@ +[ + "Clappy Trio", + "Sneaky Spirits", + "Rhythm Tweezers", + "Glee Club", + "Rhythm Rally", + "Fillbots", + "Shoot-'em-Up", + "Air Rally", + "Micro-Row", + "Flipper-Flop", + "Figure Fighter", + "Fruit Basket", + "First Contact", + "Catchy Tune", + "LumBEARjack", + "Spaceball", + "Clappy Trio 2", + "Sneaky Spirits 2", + "Rhythm Tweezers 2", + "Bouncy Road", + "Marching Orders", + "Night Walk", + "Quiz Show", + "Bunny Hop", + "Rat Race", + "Power Calligraphy", + "Space Dance", + "Tap Trial", + "Ninja Bodyguard", + "Airboarder", + "Lockstep", + "Blue Birds", + "Dazzles", + "Freeze Frame", + "Glee Club 2", + "Frog Hop", + "Fan Club", + "Dog Ninja", + "Rhythm Rally 2", + "Fillbots 2", + "Shoot-'em-Up 2", + "Big Rock Finish", + "Munchy Monk", + "Built to Scale", + "Air Rally 2", + "Exhibition Match", + "Flockstep", + "Cheer Readers", + "Double Date", + "Catch of the Day", + "Micro-Row 2", + "Fork Lifter", + "Hole in One", + "Flipper-Flop 2", + "Ringside", + "Working Dough", + "Figure Fighter", + "Love Rap", + "Bossa Nova", + "Screwbots", + "Launch Party", + "Board Meeting", + "Samurai Slice", + "See-Saw", + "Packing Pests", + "Monkey Watch", + "Blue Bear", + "Animal Acrobat", + "Tongue Lashing", + "Super Samurai Slice", + "Fruit Basket 2", + "Second Contact", + "Pajama Party", + "Catchy Tune 2", + "Sumo Brothers", + "Tangotronic 3000", + "Kitties!", + "LumBEARjack 2", + "Snappy Trio", + "Cosmic Dance", + "Tap Trial 2", + "Jumpin' Jazz", + "Fan Club 2", + "Cosmic Rhythm Rally", + "Hole in One 2", + "Working Dough 2", + "Figure Fighter 3", + "Jungle Gymnast", + "Super Samurai Slice 2", + "Karate Man", + "Karate Man Returns!", + "Karate Man Kicks!", + "Karate Man Combos!", + "Karate Man Senior", + "Lush Remix", + "Final Remix", + "Honeybee Remix", + "Machine Remix", + "Citrus Remix", + "Donut Remix", + "Barbershop Remix", + "Songbird Remix", + "Left-Hand Remix", + "Right-Hand Remix" +] \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..8dde375 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'treasury' + diff --git a/src/main/kotlin/rhmodding/treasury/TreasuryApp.kt b/src/main/kotlin/rhmodding/treasury/TreasuryApp.kt new file mode 100644 index 0000000..e0588be --- /dev/null +++ b/src/main/kotlin/rhmodding/treasury/TreasuryApp.kt @@ -0,0 +1,327 @@ +package rhmodding.treasury + +import javafx.collections.FXCollections.observableArrayList +import javafx.collections.ObservableList +import javafx.geometry.Pos +import javafx.scene.control.* +import javafx.scene.layout.VBox +import javafx.stage.DirectoryChooser +import rhmodding.treasury.model.* +import tornadofx.* +import java.io.File + +class TreasuryApp : App(TreasuryView::class) + +class TreasuryView : View("Treasury") { + override val root = VBox() + var path: String = "/" + + lateinit var worldList: ObservableList + + lateinit var courseList: ObservableList + lateinit var courseListView: ListView + lateinit var superHardBox: CheckBox + lateinit var flowBallsSpinner: Spinner + lateinit var unkCourseSpinner: Spinner + lateinit var unlockList: ObservableList + lateinit var unlockListView: ListView + + lateinit var groupList: ObservableList + lateinit var groupListView: ListView + lateinit var randomBox: CheckBox + lateinit var goalBox: ComboBox + lateinit var goalArgSpinner: Spinner + lateinit var tempoSpinner: Spinner + + lateinit var gameList: ObservableList + lateinit var gameListView: ListView + lateinit var gameBox: ComboBox + lateinit var unkSpinner: Spinner + lateinit var gameLabel: Label + + var currentWorld: TreasureWorld? = null + var currentCourse: TreasureCourse? = null + var currentGroup: TreasureGroup? = null + var currentGame: TreasureGame? = null + var treasureData: TreasureData? = null + + fun updateWorld(it: String) { + currentWorld = treasureData?.worlds?.get(worldList.indexOf(it)) + courseList.removeAll { true } + courseList.addAll(currentWorld?.courses?: listOf()) + updateCourse(courseList[0]) + } + + fun updateCourse(it: TreasureCourse) { + currentCourse = it + groupList.removeAll { true } + groupList.addAll(currentCourse?.groups?: listOf()) + updateGroup(groupList[0]) + superHardBox.isSelected = it.superHard + flowBallsSpinner.valueFactory.value = it.flowBalls.toInt() and 0xFF + unkCourseSpinner.valueFactory.value = it.unk.toInt() and 0xFF + unlockList.removeAll {true} + unlockList.addAll(it.coursesToUnlock.map { CourseNumber(it.toInt()) }) + } + + fun updateGroup(it: TreasureGroup) { + currentGroup = it + gameList.removeAll { true } + gameList.addAll(currentGroup?.games?: listOf()) + updateGame(gameList[0]) + randomBox.isSelected = it.random + goalBox.value = it.goal + goalArgSpinner.valueFactory.value = it.goalArg.toInt() + tempoSpinner.valueFactory.value = it.tempo.toInt() and 0xFF + } + + fun updateGame(it: TreasureGame) { + currentGame = it + gameBox.value = GameNumber(it.id) + unkSpinner.valueFactory.value = it.unkPercentage.toInt() and 0xFF + } + + init { + with (root) { + borderpane { + top = menubar { + menu("File") { + item("Open...") { + action { + val dirChooser = DirectoryChooser() + dirChooser.title = "Choose a directory contining the contents of treasure_world_data.zlib" + dirChooser.initialDirectory = File(path) + + val f = dirChooser.showDialog(null) + if (f != null) { + path = f.parent + val courses = mutableListOf() + f.listFiles({ _, name -> name.startsWith("world_data_") }).mapTo(courses) { TreasureCourse(it) } + courses.sortBy { it.id } + val l = f.listFiles { _, name -> name.startsWith("course_data.bin") } + if (l.isNotEmpty()) { + treasureData = TreasureData(l[0], courses) + } + } + } + } + item("Save...") { + action { + val dirChooser = DirectoryChooser() + dirChooser.title = "Choose a directory to save your changes in." + dirChooser.initialDirectory = File(path) + + val f = dirChooser.showDialog(null) + if (f != null) { + path = f.parent + val c_d = File(f, "course_data.bin") + c_d.writeBytes(treasureData!!.toBytes().toByteArray()) + for (w in treasureData!!.worlds) { + for (c in w.courses) { + val w_d = File(f, "world_data_${c.id}.bin") + w_d.writeBytes(c.toBytes().toByteArray()) + } + } + } + } + } + } + } + bottom = hbox(spacing = 6) { + paddingAll = 10 + vbox(spacing = 5) { + label("Worlds") + worldList = observableArrayList("0 - Saffron World", "1 - Saltwater World", "2 - Paprika World") + listview(worldList) { + onUserSelect(1) { + updateWorld(it) + } + } + } + vbox(spacing = 5) { + maxWidth = prefWidth + label("Courses") + courseList = observableArrayList() + courseListView = listview(courseList) { + onUserSelect(1) { + updateCourse(it) + } + } + superHardBox = checkbox("Super Hard?") { + selectedProperty().addListener { _, _, newValue -> + currentCourse?.superHard = newValue + } + } + hbox(spacing = 0) { + label("Flow Ball Reward: ") + flowBallsSpinner = spinner(0, 255, 1, 1, true) { + prefWidth = 70.0 + valueProperty().addListener { _, _, newValue -> + currentCourse?.flowBalls = newValue.toByte() + } + } + } + hbox(spacing = 0) { + label("Unknown Value: ") + unkCourseSpinner = spinner(0, 255, 3, 1, true) { + prefWidth = 70.0 + valueProperty().addListener { _, _, newValue -> + currentCourse?.unk = newValue.toByte() + } + } + } + label("Courses Needed to Unlock:") + unlockList = observableArrayList() + unlockListView = listview(unlockList) { + prefHeight = 100.0 + } + hbox(spacing = 0) { + button("Add") { + action { + unlockList.add(CourseNumber(0)) + currentCourse?.coursesToUnlock?.add(0) + } + } + button("Remove") { + action { + val item = unlockListView.selectedItem + unlockList.remove(item) + currentCourse?.coursesToUnlock?.remove(item?.id?.toShort()?:-1) + } + } + } + var lbl: Label? = null + hbox(spacing = 5) { + spinner(0, 39, 0, 1, true) { + prefWidth = 70.0 + valueProperty().addListener { _, _, newValue -> + lbl?.text = TreasureCourse.COURSENAMES[newValue] + unlockListView.selectedItem?.id = newValue + unlockListView.refresh() + currentCourse?.coursesToUnlock?.removeAll{true} + currentCourse?.coursesToUnlock?.addAll(unlockList.map { it.id.toShort() }) + } + } + lbl = label("") + } + } + vbox(spacing = 5) { + label("Groups") + groupList = observableArrayList() + groupListView = listview(groupList) { + onUserSelect(1) { + updateGroup(it) + } + } + hbox(spacing = 10) { + button("Add") { + action { + val group = TreasureGroup(null) + currentCourse?.groups?.add(group) + groupList.add(group) + } + } + button("Remove") { + if (groupList.size > 1) { + groupList.remove(currentGroup) + currentCourse?.groups?.remove(currentGroup) + } + } + } + randomBox = checkbox("Random?") { + selectedProperty().addListener { _, _, newValue -> + currentGroup?.random = newValue + groupListView.refresh() + } + } + hbox(spacing = 0) { + alignment = Pos.CENTER_LEFT + label("Goal: ") + goalBox = combobox(null, listOf(GoalType.Points, GoalType.Lives, GoalType.Monster)) { + valueProperty().addListener { _, _, newValue -> + currentGroup?.goal = newValue + groupListView.refresh() + } + } + label(" ") + goalArgSpinner = spinner(0, 65535, 0, 1, true) { + prefWidth = 70.0 + valueProperty().addListener { _, _, newValue -> + currentGroup?.goalArg = newValue.toShort() + groupListView.refresh() + } + } + } + hbox(spacing = 0) { + alignment = Pos.CENTER_LEFT + label("Tempo: ") + tempoSpinner = spinner(1, 255, 100, 1, true) { + prefWidth = 70.0 + valueProperty().addListener { _, _, newValue -> + currentGroup?.tempo = newValue.toByte() + groupListView.refresh() + } + } + label("%") + } + } + vbox(spacing = 5) { + label("Games") + gameList = observableArrayList() + gameListView = listview(gameList) { + onUserSelect(1) { + updateGame(it) + } + } + hbox(spacing = 10) { + button("Add") { + action { + val game = TreasureGame(0, 100) + currentGroup?.games?.add(game) + gameList.add(game) + groupListView.refresh() + } + } + button("Remove") { + action { + if (gameList.size > 1) { + gameList.remove(currentGame) + currentGroup?.games?.remove(currentGame) + groupListView.refresh() + } + } + } + } + hbox(spacing = 0) { + alignment = Pos.CENTER_LEFT + label("Game: ") + gameBox = combobox { + for (i in 0 until 104) { + items.add(GameNumber(i.toShort())) + } + valueProperty().addListener { _, _, newValue -> + currentGame?.id = newValue.id + gameListView.refresh() + courseListView.refresh() + } + } + } + hbox(spacing = 0) { + alignment = Pos.CENTER_LEFT + label("Unknown: ") + unkSpinner = spinner(0, 255, 100, 1, true) { + prefWidth = 70.0 + valueProperty().addListener { _, _, newValue -> + currentGame?.unkPercentage = newValue.toByte() + gameListView.refresh() + groupListView.refresh() + } + } + label("%") + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/rhmodding/treasury/model/TreasureCourse.kt b/src/main/kotlin/rhmodding/treasury/model/TreasureCourse.kt new file mode 100644 index 0000000..47a4441 --- /dev/null +++ b/src/main/kotlin/rhmodding/treasury/model/TreasureCourse.kt @@ -0,0 +1,70 @@ +package rhmodding.treasury.model + +import com.github.salomonbrys.kotson.fromJson +import com.google.gson.Gson +import java.io.File +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.file.Files +import java.nio.file.Paths +import kotlin.experimental.and + +class TreasureCourse(file: File) { + companion object { + val COURSENAMES: List = Gson().fromJson(File("./coursenames.json").readText()) + } + var id: Short + var superHard: Boolean + var flowBalls: Byte + val coursesToUnlock = mutableListOf() + var unk: Byte + val groups = mutableListOf() + + init { + val b = Files.readAllBytes(Paths.get(file.absolutePath)) + val buf = ByteBuffer.wrap(b) + buf.order(ByteOrder.LITTLE_ENDIAN) + buf.position(0) + id = buf.short + superHard = buf.get() != 0.toByte() + flowBalls = buf.get() + val amount = buf.get() + for (i in 0 until amount) { + coursesToUnlock.add(buf.short) + } + unk = buf.get() + val gameAmount = buf.get() + for (i in 0 until gameAmount) { + groups.add(TreasureGroup(buf)) + } + } + + fun toBytes(): List { + val l = mutableListOf() + l.add((id and 0xFF).toByte()) + l.add((id.toInt() ushr 8).toByte()) + l.add(if (superHard) 1.toByte() else 0.toByte()) + l.add(flowBalls) + l.add(coursesToUnlock.size.toByte()) + for (c in coursesToUnlock){ + l.add((c and 0xFF).toByte()) + l.add((c.toInt() ushr 8).toByte()) + } + l.add(unk) + l.add(groups.size.toByte()) + for (g in groups) { + l.addAll(g.toBytes()) + } + return l + } + + override fun toString(): String { + return """$id (${COURSENAMES[id.toInt()]})""" + } +} + +data class CourseNumber(var id: Int) { + override fun toString(): String { + return """$id (${TreasureCourse.COURSENAMES[id.toInt()]})""" + } +} \ No newline at end of file diff --git a/src/main/kotlin/rhmodding/treasury/model/TreasureData.kt b/src/main/kotlin/rhmodding/treasury/model/TreasureData.kt new file mode 100644 index 0000000..3ba4bf9 --- /dev/null +++ b/src/main/kotlin/rhmodding/treasury/model/TreasureData.kt @@ -0,0 +1,30 @@ +package rhmodding.treasury.model + +import java.io.File +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +class TreasureData(file: File, courses: List) { + val worlds = mutableListOf() + init { + val b = Files.readAllBytes(Paths.get(file.absolutePath)) + val buf = ByteBuffer.wrap(b) + buf.order(ByteOrder.LITTLE_ENDIAN) + buf.position(0) + while (buf.hasRemaining()) { + val world = TreasureWorld() + val amount = buf.get() + for (i in 0 until amount) { + world.courses.add(courses[buf.short.toInt()]) + } + worlds.add(world) + } + } + + fun toBytes(): List { + return worlds.map { it.toBytes() }.flatten() + } +} \ No newline at end of file diff --git a/src/main/kotlin/rhmodding/treasury/model/TreasureGroup.kt b/src/main/kotlin/rhmodding/treasury/model/TreasureGroup.kt new file mode 100644 index 0000000..173a917 --- /dev/null +++ b/src/main/kotlin/rhmodding/treasury/model/TreasureGroup.kt @@ -0,0 +1,82 @@ +package rhmodding.treasury.model + +import com.github.salomonbrys.kotson.fromJson +import com.google.gson.Gson +import java.io.File +import java.nio.ByteBuffer +import kotlin.experimental.and + +class TreasureGroup(buf: ByteBuffer?) { + var random: Boolean = false + var choices: Byte = 1 + var goal: GoalType = GoalType.Points + var goalArg: Short = 0 + var tempo: Byte = 100 + val games = mutableListOf() + + init { + if (buf == null) { + games.add(TreasureGame(0, 100)) + } else { + random = buf.get() != 0.toByte() + choices = buf.get() + goal = GoalType.fromByte(buf.get())!! + goalArg = buf.short + tempo = buf.get() + val gameAmount = buf.get() + for (i in 0 until gameAmount) { + games.add(TreasureGame(buf.short, buf.get())) + } + } + } + + fun toBytes(): List { + choices = if (random) 1.toByte() else games.size.toByte() + val l = mutableListOf() + l.add(if (random) 1.toByte() else 0.toByte()) + l.add(choices) + l.add(goal.n) + l.add((goalArg and 0xFF).toByte()) + l.add((goalArg.toInt() ushr 8).toByte()) + l.add(tempo) + l.add(games.size.toByte()) + for (g in games) { + l.add((g.id and 0xFF).toByte()) + l.add((g.id.toInt() ushr 8).toByte()) + l.add(g.unkPercentage) + } + return l + } + + override fun toString(): String { + return """${if (games.size == 1) {"${games[0].id} (${ TreasureGame.GAMENAMES[games[0].id.toInt()]})"} else if (random) "Random" else "Multiple Choice"} + $goal, $goalArg + ${tempo.toInt() and 0xFF}% tempo""" + } +} + +enum class GoalType(val n: Byte) { + Points(0), + Lives(1), + Monster(2); + companion object { + private val map = GoalType.values().associateBy(GoalType::n); + fun fromByte(type: Byte) = map[type] + } +} + +data class TreasureGame(var id: Short, var unkPercentage: Byte) { + companion object { + val GAMENAMES: List = Gson().fromJson(File("./gamenames.json").readText()) + } + + override fun toString(): String { + return "$id (${GAMENAMES[id.toInt()]}); $unkPercentage%" + } +} + +data class GameNumber(var id: Short) { + override fun toString(): String { + return "$id (${TreasureGame.GAMENAMES[id.toInt()]})" + } +} \ No newline at end of file diff --git a/src/main/kotlin/rhmodding/treasury/model/TreasureWorld.kt b/src/main/kotlin/rhmodding/treasury/model/TreasureWorld.kt new file mode 100644 index 0000000..c4187d8 --- /dev/null +++ b/src/main/kotlin/rhmodding/treasury/model/TreasureWorld.kt @@ -0,0 +1,17 @@ +package rhmodding.treasury.model + +import kotlin.experimental.and + +class TreasureWorld { + val courses = mutableListOf() + + fun toBytes(): List { + val l = mutableListOf() + l.add(courses.size.toByte()) + for (c in courses) { + l.add((c.id and 0xFF).toByte()) + l.add((c.id.toInt() ushr 8).toByte()) + } + return l + } +} \ No newline at end of file