diff --git a/Zelda no Densetsu - Kamigami no Triforce (Japan).json b/Zelda no Densetsu - Kamigami no Triforce (Japan).json deleted file mode 100644 index 17a6173..0000000 --- a/Zelda no Densetsu - Kamigami no Triforce (Japan).json +++ /dev/null @@ -1,196 +0,0 @@ -{ - "code" : { - "002111" : { - "label" : "layer3X" - }, - "002112" : { - "label" : "layer3Y" - }, - "002140" : { - "label" : "apuIo0" - }, - "002141" : { - "label" : "apuIo1" - }, - "002142" : { - "label" : "apuIo2" - }, - "002143" : { - "label" : "apuIo3" - }, - "00420c" : { - "label" : "hdmaEnable" - }, - "008000" : { - "label" : "ResetVector" - }, - "00801b" : { - "comment" : "\\ Turn off emulation mode" - }, - "00801c" : { - "comment" : "/" - }, - "00801f" : { - "comment" : "\\ Set direct page" - }, - "008022" : { - "comment" : "/" - }, - "008023" : { - "comment" : "\\ Set stack position" - }, - "008026" : { - "comment" : "/" - }, - "008034" : { - "label" : "MainGameLoop", - "comment" : "Wait for NMI" - }, - "00805d" : { - "comment" : "Clear NMI flag" - }, - "0080b5" : { - "label" : "JumpToGameMode", - "comment" : "Y = Current game mode" - }, - "0080b7" : { - "comment" : "\\ Load routine low byte" - }, - "0080ba" : { - "comment" : "/" - }, - "0080bc" : { - "comment" : "\\ Load routine mid byte" - }, - "0080bf" : { - "comment" : "/" - }, - "0080c1" : { - "comment" : "\\ Load routine high byte" - }, - "0080c4" : { - "comment" : "/" - }, - "0080c6" : { - "flags" : [ { - "flagType" : "JmpIndirectLongInterleavedTable", - "start" : "008061", - "entries" : 28 - } ] - }, - "0080c9" : { - "label" : "NmiVector" - }, - "00822c" : { - "label" : "UnusedVector" - }, - "0082d8" : { - "label" : "IrqVector" - }, - "00841e" : { - "label" : "ClearOam" - }, - "00879c" : { - "comment" : "Preserve Y value for later", - "flags" : [ { - "flagType" : "NonReturningRoutine" - } ] - }, - "00879e" : { - "comment" : "Y = Ret.Bank" - }, - "00879f" : { - "comment" : "$02 = Ret.Bank" - }, - "0087a3" : { - "comment" : "\\" - }, - "0087a6" : { - "comment" : "|" - }, - "0087a8" : { - "comment" : "| Y = In.A * 3" - }, - "0087a9" : { - "comment" : "|" - }, - "0087ab" : { - "comment" : "/" - }, - "0087ac" : { - "comment" : "\\ $03-$04 = Ret.Offset" - }, - "0087ad" : { - "comment" : "/" - }, - "0087af" : { - "comment" : "Increase Y to compensate for Ret being off by one" - }, - "0087b0" : { - "comment" : "\\" - }, - "0087b2" : { - "comment" : "|" - }, - "0087b4" : { - "comment" : "| Load target pointer into $00-03 (last byte unused)" - }, - "0087b5" : { - "comment" : "|" - }, - "0087b7" : { - "comment" : "/" - }, - "0087bb" : { - "comment" : "Restore initial Y value" - }, - "0087bd" : { - "comment" : "Jump to pointer" - }, - "008901" : { - "comment" : "\\" - }, - "008903" : { - "comment" : "|" - }, - "008905" : { - "comment" : "| Write #$19:8000 to $00" - }, - "008907" : { - "comment" : "|" - }, - "008909" : { - "comment" : "|" - }, - "00890b" : { - "comment" : "/" - }, - "00ffff" : { - "label" : "CrashVector" - }, - "0287d0" : { - "flags" : [ { - "flagType" : "JslTableRoutine", - "entries" : 4 - } ] - }, - "029ee3" : { - "label" : "GM_TriforceRoom" - }, - "0cc115" : { - "flags" : [ { - "flagType" : "JslTableRoutine", - "entries" : 12 - } ] - }, - "7e0010" : { - "label" : "gameMode" - }, - "7e0011" : { - "label" : "subGameMode" - }, - "7e0012" : { - "label" : "nmiExecuted" - } - } -} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/Grid.kt b/src/main/java/com/smallhacker/disbrowser/Grid.kt index 7ca482f..84f3c00 100644 --- a/src/main/java/com/smallhacker/disbrowser/Grid.kt +++ b/src/main/java/com/smallhacker/disbrowser/Grid.kt @@ -1,6 +1,8 @@ package com.smallhacker.disbrowser import com.smallhacker.disbrowser.asm.* +import com.smallhacker.disbrowser.game.Game +import com.smallhacker.disbrowser.game.GameData class Grid { private val arrowCells = HashMap, HtmlNode?>() @@ -45,7 +47,6 @@ class Grid { arrowClasses[x to y] = "arrow arrow-$dir-middle" } arrowClasses[x to y2] = "arrow arrow-$dir-end" - //arrowCells[x to yStart] = a().addClass("arrow-link").attr("href", "#${to.toSimpleString()}") arrowCells[x to yEnd] = div().addClass("arrow-head") } @@ -59,8 +60,9 @@ class Grid { .first() } - fun add(ins: CodeUnit, metadata: Metadata, disassembly: Disassembly) { + fun add(ins: CodeUnit, game: Game, disassembly: Disassembly) { val presentedAddress = ins.presentedAddress + val gameData = game.gameData if (nextAddress != null) { if (presentedAddress != nextAddress) { @@ -73,12 +75,12 @@ class Grid { addresses[presentedAddress] = y val (address, bytes, label, primaryMnemonic, secondaryMnemonic, suffix, operands, state, comment, labelAddress) - = ins.print(metadata) + = ins.print(gameData) add(y, ins.address, text(address ?: ""), text(bytes), - editableField(presentedAddress, "label", label), + editableField(game, presentedAddress, "label", label), fragment { if (secondaryMnemonic == null) { text(primaryMnemonic) @@ -94,26 +96,24 @@ class Grid { if (labelAddress == null) { text(operands ?: "") } else { - val currentLabel = metadata[labelAddress]?.label - editablePopupField(labelAddress, "label", operands, currentLabel) + val currentLabel = gameData[labelAddress]?.label + editablePopupField(game, labelAddress, "label", operands, currentLabel) } } else { val local = link.address in disassembly val url = when { local -> "#${link.address.toSimpleString()}" - else -> "/${link.address.toSimpleString()}/${link.urlString}" + else -> "/${game.id}/${link.address.toSimpleString()}/${link.urlString}" } - //operands = metadata[link.address]?.label ?: operands - a { text(operands ?: "") }.attr("href", url) } }, text(state ?: ""), - editableField(presentedAddress, "comment", comment) + editableField(game, presentedAddress, "comment", comment) ) if (ins.opcode.continuation == Continuation.NO) { @@ -121,15 +121,16 @@ class Grid { } } - private fun editableField(address: SnesAddress, type: String, value: String?): HtmlNode { + private fun editableField(game: Game, address: SnesAddress, type: String, value: String?): HtmlNode { return input(value = value ?: "") .addClass("field-$type") .addClass("field-editable") .attr("data-field", type) + .attr("data-game", game.id) .attr("data-address", address.toSimpleString()) } - private fun HtmlArea.editablePopupField(address: SnesAddress, type: String, displayValue: String?, editValue: String?) { + private fun HtmlArea.editablePopupField(game: Game, address: SnesAddress, type: String, displayValue: String?, editValue: String?) { span { text(displayValue ?: "") span {}.addClass("field-editable-popup-icon") @@ -138,6 +139,7 @@ class Grid { .addClass("field-editable-popup") .attr("data-field", type) .attr("data-value", editValue ?: "") + .attr("data-game", game.id) .attr("data-address", address.toSimpleString()) } diff --git a/src/main/java/com/smallhacker/disbrowser/HtmlBuilder.kt b/src/main/java/com/smallhacker/disbrowser/HtmlBuilder.kt deleted file mode 100644 index 60e34e8..0000000 --- a/src/main/java/com/smallhacker/disbrowser/HtmlBuilder.kt +++ /dev/null @@ -1,39 +0,0 @@ -//package com.smallhacker.disbrowser -// -//class HtmlContext(val out: StringBuilder) -// -//fun html(inner: HtmlContext.() -> Unit = {}): String { -// val html = HtmlContext(StringBuilder().append("")) -// element(html, "html") { inner(html) } -// return html.out.toString() -//} -// -//private fun element(html: HtmlContext, tag: String, inner: HtmlContext.() -> Unit) = element(html, tag, inner, *emptyArray()) -// -//private fun element(html: HtmlContext, tag: String, inner: HtmlContext.() -> Unit, vararg args: String) { -// html.out.append("<$tag") -// html.out.append(args.asSequence().map { " $it" }.joinToString()) -// html.out.append(">") -// html.inner() -// html.out.append("") -//} -// -// -//fun HtmlContext.text(text: String) { -// this.out.append(text) -//} -// -//fun HtmlContext.head(inner: HtmlContext.() -> Unit = {}) = element(this, "head", inner) -//fun HtmlContext.title(inner: HtmlContext.() -> Unit = {}) = element(this, "title", inner) -//fun HtmlContext.body(inner: HtmlContext.() -> Unit = {}) = element(this, "body", inner) -//fun HtmlContext.style(inner: HtmlContext.() -> Unit = {}) = element(this, "style", inner) -//fun HtmlContext.link(href: String, inner: HtmlContext.() -> Unit = {}) = element(this, "link", inner, "rel=\"stylesheet\" href=\"$href\"") -//fun HtmlContext.div(inner: HtmlContext.() -> Unit = {}) = element(this, "div", inner) -//fun HtmlContext.div(cssClass: String, inner: HtmlContext.() -> Unit = {}) = element(this, "div", inner, "class=\"$cssClass\"") -// -//fun HtmlContext.table(inner: HtmlContext.() -> Unit = {}) = element(this, "table", inner) -//fun HtmlContext.tr(inner: HtmlContext.() -> Unit = {}) = element(this, "tr", inner) -//fun HtmlContext.tr(cssClass: String?, inner: HtmlContext.() -> Unit = {}) = element(this, "tr", inner, (if (cssClass == null) "" else "class=\"$cssClass\"")) -//fun HtmlContext.td(inner: HtmlContext.() -> Unit = {}) = element(this, "td", inner) -//fun HtmlContext.td(cssClass: String?, inner: HtmlContext.() -> Unit = {}) = element(this, "td", inner, (if (cssClass == null) "" else "class=\"$cssClass\"")) -//fun HtmlContext.a(href: String, inner: HtmlContext.() -> Unit = {}) = element(this, "a", inner, "href=\"$href\"") \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/Main.java b/src/main/java/com/smallhacker/disbrowser/Main.java deleted file mode 100644 index b790f02..0000000 --- a/src/main/java/com/smallhacker/disbrowser/Main.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.smallhacker.disbrowser; - -import org.glassfish.grizzly.http.server.HttpServer; -import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory; -import org.glassfish.jersey.server.ResourceConfig; - -import java.io.IOException; -import java.net.URI; - -/** - * Main class. - * - */ -public class Main { - // Base URI the Grizzly HTTP server will listen on - public static final String BASE_URI = "http://localhost:8080/"; - - /** - * Starts Grizzly HTTP server exposing JAX-RS resources defined in this application. - * @return Grizzly HTTP server. - */ - public static HttpServer startServer() { - // create a resource config that scans for JAX-RS resources and providers - // in com.smallhacker.disbrowser.resource package - final ResourceConfig rc = new ResourceConfig().packages("com.smallhacker.disbrowser.resource"); - - // create and start a new instance of grizzly http server - // exposing the Jersey application at BASE_URI - return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc); - } - - /** - * Main method. - * @param args - * @throws IOException - */ - public static void main(String[] args) throws IOException { - final HttpServer server = startServer(); - System.out.println(String.format("Jersey app started with WADL available at " - + "%sapplication.wadl\nHit enter to stop it...", BASE_URI)); - System.in.read(); - server.stop(); - } -} - diff --git a/src/main/java/com/smallhacker/disbrowser/Main.kt b/src/main/java/com/smallhacker/disbrowser/Main.kt new file mode 100644 index 0000000..5fba940 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/Main.kt @@ -0,0 +1,36 @@ +package com.smallhacker.disbrowser + +import com.smallhacker.disbrowser.game.addGameSource +import org.glassfish.grizzly.http.server.HttpServer +import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory +import org.glassfish.jersey.server.ResourceConfig + +import java.io.IOException +import java.lang.IllegalArgumentException +import java.net.URI +import java.nio.file.Path +import java.nio.file.Paths + +object Main { + private const val BASE_URI = "http://localhost:8080/" + + private fun startServer(path: Path): HttpServer { + val rc = ResourceConfig() + .packages("com.smallhacker.disbrowser.resource") + .addGameSource(path) + return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc) + } + + @Throws(IOException::class) + @JvmStatic + fun main(args: Array) { + if (args.size != 1) { + throw IllegalArgumentException("Game data directory needed") + } + val server = startServer(Paths.get(args[0])) + println("Server started. Press any key to stop.") + System.`in`.read() + server.stop() + } +} + diff --git a/src/main/java/com/smallhacker/disbrowser/Service.kt b/src/main/java/com/smallhacker/disbrowser/Service.kt index d6fcfdb..2ca404e 100644 --- a/src/main/java/com/smallhacker/disbrowser/Service.kt +++ b/src/main/java/com/smallhacker/disbrowser/Service.kt @@ -1,10 +1,10 @@ package com.smallhacker.disbrowser import com.smallhacker.disbrowser.asm.* +import com.smallhacker.disbrowser.game.Game import com.smallhacker.disbrowser.disassembler.Disassembler -import com.smallhacker.disbrowser.util.jsonFile +import com.smallhacker.disbrowser.game.GameData import com.smallhacker.disbrowser.util.toUInt24 -import java.nio.file.Paths import kotlin.reflect.KMutableProperty1 private val RESET_VECTOR_LOCATION = address(0x00_FFFC) @@ -25,37 +25,26 @@ private val VECTORS = listOf( ) object Service { - private const val romName = "Zelda no Densetsu - Kamigami no Triforce (Japan)" - private val romDir = Paths.get("""P:\Emulation\ROMs\SNES""") - private val metaDir = Paths.get("""P:\Programming\disbrowser""") - private val metaFile = jsonFile(metaDir.resolve("$romName.json"), true) - private val metadata by lazy { metaFile.load() } - - private val snesMemory by lazy { - val path = romDir.resolve("$romName.sfc") - SnesLoRom(loadRomData(path)) - } - - fun showDisassemblyFromReset(): HtmlNode? { - val resetVector = snesMemory.getWord(RESET_VECTOR_LOCATION) + fun showDisassemblyFromReset(game: Game): HtmlNode? { + val resetVector = game.memory.getWord(RESET_VECTOR_LOCATION) val fullResetVector = resetVector!!.toUInt24() val initialAddress = SnesAddress(fullResetVector) val flags = VagueNumber(0x30u) - return showDisassembly(initialAddress, flags) + return showDisassembly(game, initialAddress, flags) } - fun showDisassembly(initialAddress: SnesAddress, flags: VagueNumber): HtmlNode? { - val initialState = State(memory = snesMemory, address = initialAddress, flags = flags, metadata = metadata) - val disassembly = Disassembler.disassemble(initialState, metadata, false) + fun showDisassembly(game: Game, initialAddress: SnesAddress, flags: VagueNumber): HtmlNode? { + val initialState = State(memory = game.memory, address = initialAddress, flags = flags, gameData = game.gameData) + val disassembly = Disassembler.disassemble(initialState, game.gameData, false) - return print(disassembly, metadata) + return print(disassembly, game) } - private fun print(disassembly: Disassembly, metadata: Metadata): HtmlNode { + private fun print(disassembly: Disassembly, game: Game): HtmlNode { val grid = Grid() disassembly.forEach { - grid.add(it, metadata, disassembly) + grid.add(it, game, disassembly) } disassembly.asSequence() .mapNotNull { @@ -70,28 +59,27 @@ object Service { return grid.output() } - fun updateMetadata(address: SnesAddress, field: KMutableProperty1, value: String) { + fun updateMetadata(game: Game, address: SnesAddress, field: KMutableProperty1, value: String) { if (value.isEmpty()) { - if (address in metadata) { - doUpdateMetadata(address, field, null) + if (address in game.gameData) { + doUpdateMetadata(game, address, field, null) } } else { - doUpdateMetadata(address, field, value) + doUpdateMetadata(game, address, field, value) } } - private fun doUpdateMetadata(address: SnesAddress, field: KMutableProperty1, value: String?) { - val line = metadata.getOrCreate(address) + private fun doUpdateMetadata(game: Game, address: SnesAddress, field: KMutableProperty1, value: String?) { + val line = game.gameData.getOrCreate(address) field.set(line, value) - metadata.cleanUp() - metaFile.save(metadata) + game.saveGameData() } - fun getVectors() = VECTORS.asSequence() + fun getVectors(game: Game) = VECTORS.asSequence() .map { (vectorLocation: SnesAddress, name: String ) -> - val codeLocation = SnesAddress(snesMemory.getWord(vectorLocation)!!.toUInt24()) - val label = metadata[codeLocation]?.label + val codeLocation = SnesAddress(game.memory.getWord(vectorLocation)!!.toUInt24()) + val label = game.gameData[codeLocation]?.label ?: codeLocation.toFormattedString() Vector(vectorLocation, codeLocation, name, label) } diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt b/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt index 223bf3a..5353ef4 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Instruction.kt @@ -1,5 +1,6 @@ package com.smallhacker.disbrowser.asm +import com.smallhacker.disbrowser.game.GameData import com.smallhacker.disbrowser.util.* interface CodeUnit { @@ -16,7 +17,7 @@ interface CodeUnit { val opcode: Opcode val lengthSuffix: String? - val memory: SnesMapper + val memory: SnesMemory fun operandByte(index: UInt): UByte = bytes[opcode.operandIndex + index] @@ -57,7 +58,7 @@ class DataBlock( override val presentedAddress: SnesAddress, override val relativeAddress: SnesAddress, override val linkedState: State?, - override val memory: SnesMapper + override val memory: SnesMemory ) : CodeUnit { override val nextPresentedAddress: SnesAddress get() = presentedAddress + operandLength.toInt() @@ -122,21 +123,21 @@ class Instruction(override val bytes: ValidMemorySpace, override val opcode: Opc } } -fun CodeUnit.print(metadata: Metadata? = null): PrintedCodeUnit { +fun CodeUnit.print(gameData: GameData? = null): PrintedCodeUnit { val mnemonic = opcode.mnemonic val primaryMnemonic = mnemonic.displayName val secondaryMnemonic = mnemonic.alternativeName var suffix = lengthSuffix - var operands = metadata?.let { opcode.mode.printWithLabel(this, it) } + var operands = gameData?.let { opcode.mode.printWithLabel(this, it) } if (operands == null) { operands = opcode.mode.printRaw(this) suffix = null } val state = postState?.toString() - val label = address?.let { metadata?.get(it)?.label } - val comment = address?.let { metadata?.get(it)?.comment } + val label = address?.let { gameData?.get(it)?.label } + val comment = address?.let { gameData?.get(it)?.comment } val formattedAddress = address?.toFormattedString() val bytes = bytesToString() diff --git a/src/main/java/com/smallhacker/disbrowser/asm/MemorySpace.kt b/src/main/java/com/smallhacker/disbrowser/asm/MemorySpace.kt index 57a6bf0..b1de980 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/MemorySpace.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/MemorySpace.kt @@ -39,11 +39,6 @@ fun ValidMemorySpace.getLong(address: UInt): UInt24 = joinBytes(this[address], t fun ValidMemorySpace.range(start: UInt, length: UInt): ValidMemorySpace = MemoryRange(this, start, length).validate()!! fun ValidMemorySpace.range(start: SnesAddress, length: UInt): ValidMemorySpace = range(start.value.toUInt(), length).validate()!! -fun loadRomData(path: Path): MemorySpace { - val bytes = Files.readAllBytes(path).toUByteArray() - return ArrayMemorySpace(bytes) -} - class ArrayMemorySpace(private val bytes: UByteArray) : MemorySpace { override val size = bytes.size.toUInt() diff --git a/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt b/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt index c76d6ab..00439b8 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt @@ -2,6 +2,7 @@ package com.smallhacker.disbrowser.asm import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonInclude +import com.smallhacker.disbrowser.game.InstructionFlag @JsonInclude(JsonInclude.Include.NON_DEFAULT) data class MetadataLine( diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt b/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt index 77eb026..46b2971 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Mode.kt @@ -1,5 +1,6 @@ package com.smallhacker.disbrowser.asm +import com.smallhacker.disbrowser.game.GameData import com.smallhacker.disbrowser.util.* interface Mode { @@ -7,7 +8,7 @@ interface Mode { val showLengthSuffix get() = true val canHaveLabel: Boolean fun operandLength(state: State): UInt? - fun printWithLabel(ins: CodeUnit, metadata: Metadata): String? = referencedAddress(ins)?.let { metadata[it]?.label } + fun printWithLabel(ins: CodeUnit, gameData: GameData): String? = referencedAddress(ins)?.let { gameData[it]?.label } fun printRaw(ins: CodeUnit): String fun referencedAddress(ins: CodeUnit): SnesAddress? } @@ -30,7 +31,7 @@ private abstract class RawWrappedMode( private val suffix: String = "" ) : Mode by parent { override val canHaveLabel = false - override fun printWithLabel(ins: CodeUnit, metadata: Metadata): String? = printRaw(ins) + override fun printWithLabel(ins: CodeUnit, gameData: GameData): String? = printRaw(ins) override fun printRaw(ins: CodeUnit) = prefix + parent.printRaw(ins) + suffix } @@ -39,7 +40,7 @@ private abstract class WrappedMode( private val prefix: String = "", private val suffix: String = "" ) : Mode by parent { - override fun printWithLabel(ins: CodeUnit, metadata: Metadata): String? = parent.printWithLabel(ins, metadata)?.let { prefix + it + suffix } + override fun printWithLabel(ins: CodeUnit, gameData: GameData): String? = parent.printWithLabel(ins, gameData)?.let { prefix + it + suffix } override fun printRaw(ins: CodeUnit) = prefix + parent.printRaw(ins) + suffix } @@ -50,7 +51,7 @@ abstract class MultiMode(private val fallback: Mode, private vararg val options: override fun operandLength(state: State) = get(state) { operandLength(state) } - override fun printWithLabel(ins: CodeUnit, metadata: Metadata): String? = get(ins.preState) { printWithLabel(ins, metadata) } + override fun printWithLabel(ins: CodeUnit, gameData: GameData): String? = get(ins.preState) { printWithLabel(ins, gameData) } override fun printRaw(ins: CodeUnit) = get(ins.preState) { printRaw(ins) } @@ -73,9 +74,9 @@ abstract class MultiMode(private val fallback: Mode, private vararg val options: } private interface DataValueType { - fun resolve(byte: UByte, state: State?, memory: SnesMapper): SnesAddress? - fun resolve(word: UShort, state: State?, memory: SnesMapper): SnesAddress? - fun resolve(long: UInt24, state: State?, memory: SnesMapper): SnesAddress? + fun resolve(byte: UByte, state: State?, memory: SnesMemory): SnesAddress? + fun resolve(word: UShort, state: State?, memory: SnesMemory): SnesAddress? + fun resolve(long: UInt24, state: State?, memory: SnesMemory): SnesAddress? val canHaveLabel: Boolean val byte: Mode get() = DataByteMode(this) @@ -85,23 +86,23 @@ private interface DataValueType { object DirectData : DataValueType { override val canHaveLabel = false - override fun resolve(byte: UByte, state: State?, memory: SnesMapper): SnesAddress? = null - override fun resolve(word: UShort, state: State?, memory: SnesMapper): SnesAddress? = null - override fun resolve(long: UInt24, state: State?, memory: SnesMapper): SnesAddress? = null + override fun resolve(byte: UByte, state: State?, memory: SnesMemory): SnesAddress? = null + override fun resolve(word: UShort, state: State?, memory: SnesMemory): SnesAddress? = null + override fun resolve(long: UInt24, state: State?, memory: SnesMemory): SnesAddress? = null } object DataPointer : DataValueType { override val canHaveLabel = true - override fun resolve(byte: UByte, state: State?, memory: SnesMapper) = state?.resolveDirectPage(byte) - override fun resolve(word: UShort, state: State?, memory: SnesMapper) = state?.resolveAbsoluteData(word) - override fun resolve(long: UInt24, state: State?, memory: SnesMapper) = memory.toCanonical(SnesAddress(long)) + override fun resolve(byte: UByte, state: State?, memory: SnesMemory) = state?.resolveDirectPage(byte) + override fun resolve(word: UShort, state: State?, memory: SnesMemory) = state?.resolveAbsoluteData(word) + override fun resolve(long: UInt24, state: State?, memory: SnesMemory) = memory.toCanonical(SnesAddress(long)) } object CodePointer : DataValueType { override val canHaveLabel = true - override fun resolve(byte: UByte, state: State?, memory: SnesMapper) = state?.resolveDirectPage(byte) - override fun resolve(word: UShort, state: State?, memory: SnesMapper) = state?.resolveAbsoluteCode(word) - override fun resolve(long: UInt24, state: State?, memory: SnesMapper) = memory.toCanonical(SnesAddress(long)) + override fun resolve(byte: UByte, state: State?, memory: SnesMemory) = state?.resolveDirectPage(byte) + override fun resolve(word: UShort, state: State?, memory: SnesMemory) = state?.resolveAbsoluteCode(word) + override fun resolve(long: UInt24, state: State?, memory: SnesMemory) = memory.toCanonical(SnesAddress(long)) } private abstract class DataValueMode(private val length: UInt, protected val type: DataValueType) : Mode { diff --git a/src/main/java/com/smallhacker/disbrowser/asm/SnesMapper.kt b/src/main/java/com/smallhacker/disbrowser/asm/SnesMemory.kt similarity index 71% rename from src/main/java/com/smallhacker/disbrowser/asm/SnesMapper.kt rename to src/main/java/com/smallhacker/disbrowser/asm/SnesMemory.kt index b811695..f5f1e78 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/SnesMapper.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/SnesMemory.kt @@ -3,8 +3,10 @@ package com.smallhacker.disbrowser.asm import com.smallhacker.disbrowser.datatype.MutableRangeMap import com.smallhacker.disbrowser.datatype.NaiveRangeMap import com.smallhacker.disbrowser.util.toUInt24 +import java.nio.file.Files +import java.nio.file.Path -abstract class SnesMapper: MemorySpace { +abstract class SnesMemory: MemorySpace { override val size = 0x100_0000u private val areas: MutableRangeMap = NaiveRangeMap() @@ -24,6 +26,15 @@ abstract class SnesMapper: MemorySpace { val offset = address.value - entry.start return entry.canonicalStart + offset.toInt() } + + companion object { + fun loadRom(path: Path): SnesMemory { + val bytes = Files.readAllBytes(path).toUByteArray() + val romSpace = ArrayMemorySpace(bytes) + // TODO: Auto-detect ROM type + return SnesLoRom(romSpace) + } + } } operator fun MemorySpace.get(address: SnesAddress) = get(address.value.toUInt()) @@ -32,7 +43,7 @@ fun MemorySpace.getLong(address: SnesAddress) = getLong(address.value.toUInt()) data class MapperEntry(val start: UInt, val canonicalStart: SnesAddress, val space: MemorySpace) -class SnesLoRom(romData: MemorySpace): SnesMapper() { +class SnesLoRom(romData: MemorySpace): SnesMemory() { init { val ram = UnreadableMemory(0x2_0000u) val ramMirror = ram.range(0x00_0000u, 0x00_2000u) @@ -58,10 +69,18 @@ class SnesLoRom(romData: MemorySpace): SnesMapper() { } for (bank in 0x40u..0x6Fu) { - val romArea = (bank shl 16) or 0x00_8000u - // Lower half unmapped - add(romArea, romArea, romData.range(pc, 0x00_8000u)) - add(romArea + high, romArea, romData.range(pc, 0x00_8000u)) + val lowerRomArea = (bank shl 16) + val upperRomArea = (bank shl 16) or 0x00_8000u + // Mirror upper and lower banks to the same ROM space. + // Some games map it this way, some leave the lower half completely unmapped + // While not 100% correct, we do the former to support as many games as possible + + // Of note, we choose to explicitly define the upper half to be the canonical half. + + add(lowerRomArea, upperRomArea, romData.range(pc, 0x00_8000u)) + add(lowerRomArea + high, upperRomArea, romData.range(pc, 0x00_8000u)) + add(upperRomArea, upperRomArea, romData.range(pc, 0x00_8000u)) + add(upperRomArea + high, upperRomArea, romData.range(pc, 0x00_8000u)) pc += 0x00_8000u } diff --git a/src/main/java/com/smallhacker/disbrowser/asm/State.kt b/src/main/java/com/smallhacker/disbrowser/asm/State.kt index 5b3ac42..38b6cfe 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/State.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/State.kt @@ -1,10 +1,11 @@ package com.smallhacker.disbrowser.asm import com.smallhacker.disbrowser.ImmStack +import com.smallhacker.disbrowser.game.GameData import com.smallhacker.disbrowser.immStack import com.smallhacker.disbrowser.util.toUInt24 -data class State(val origin: Instruction? = null, val memory: SnesMapper, val address: SnesAddress, val flags: VagueNumber = VagueNumber(), val stack: ImmStack = immStack(), val metadata: Metadata) { +data class State(val origin: Instruction? = null, val memory: SnesMemory, val address: SnesAddress, val flags: VagueNumber = VagueNumber(), val stack: ImmStack = immStack(), val gameData: GameData) { val m: Boolean? get() = flags.getBoolean(0x20u) val x: Boolean? get() = flags.getBoolean(0x10u) val db: UByte? get() = pb // TODO diff --git a/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt b/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt index 2a055a2..2f0bdb3 100644 --- a/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt +++ b/src/main/java/com/smallhacker/disbrowser/disassembler/Disassembler.kt @@ -1,11 +1,15 @@ package com.smallhacker.disbrowser.disassembler import com.smallhacker.disbrowser.asm.* +import com.smallhacker.disbrowser.game.GameData +import com.smallhacker.disbrowser.game.JmpIndirectLongInterleavedTable +import com.smallhacker.disbrowser.game.JslTableRoutine +import com.smallhacker.disbrowser.game.NonReturningRoutine import java.util.* import kotlin.collections.ArrayList object Disassembler { - fun disassemble(initialState: State, metadata: Metadata, global: Boolean): Disassembly { + fun disassemble(initialState: State, gameData: GameData, global: Boolean): Disassembly { val seen = HashSet() val queue = ArrayDeque() @@ -26,7 +30,7 @@ object Disassembler { var stop = (ins.opcode.continuation == Continuation.NO) or (ins.opcode.mode.instructionLength(state) == null) - metadata[ins.address]?.flags?.forEach { flag -> + gameData[ins.address]?.flags?.forEach { flag -> if (flag is JmpIndirectLongInterleavedTable) { if (global) { flag.readTable(state.memory) @@ -53,7 +57,7 @@ object Disassembler { val linkedState = ins.linkedState if (linkedState != null) { - metadata[linkedState.address]?.flags?.forEach { + gameData[linkedState.address]?.flags?.forEach { if (it === NonReturningRoutine) { stop = true println(ins.address.toFormattedString()) diff --git a/src/main/java/com/smallhacker/disbrowser/game/Game.kt b/src/main/java/com/smallhacker/disbrowser/game/Game.kt new file mode 100644 index 0000000..ad3f663 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/game/Game.kt @@ -0,0 +1,16 @@ +package com.smallhacker.disbrowser.game + +import com.smallhacker.disbrowser.asm.SnesMemory +import com.smallhacker.disbrowser.util.JsonFile + +class Game( + val id: String, + val memory: SnesMemory, + val gameData: GameData, + private val gameDataFile: JsonFile +) { + fun saveGameData() { + gameData.cleanUp() + gameDataFile.save(gameData) + } +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt b/src/main/java/com/smallhacker/disbrowser/game/GameData.kt similarity index 82% rename from src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt rename to src/main/java/com/smallhacker/disbrowser/game/GameData.kt index 0423576..07a0eb7 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt +++ b/src/main/java/com/smallhacker/disbrowser/game/GameData.kt @@ -1,42 +1,51 @@ -package com.smallhacker.disbrowser.asm +package com.smallhacker.disbrowser.game import com.fasterxml.jackson.annotation.JsonCreator import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonSubTypes.Type import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.smallhacker.disbrowser.asm.* import com.smallhacker.disbrowser.util.joinNullableBytes import com.smallhacker.disbrowser.util.removeIf import com.smallhacker.disbrowser.util.toUInt24 import java.util.* -class Metadata { +class GameData { @JsonProperty - private val code: MutableMap + val name: String + @JsonProperty + val path: String + @JsonProperty + private val metadata: MutableMap - constructor() { - this.code = TreeMap() + constructor(name: String, path: String) { + this.name = name + this.path = path + this.metadata = TreeMap() } @JsonCreator - private constructor(@JsonProperty code: Map) { - this.code = TreeMap(code) + private constructor(@JsonProperty name: String, @JsonProperty path: String, @JsonProperty metadata: Map) { + this.name = name + this.path = path + this.metadata = TreeMap(metadata) } - operator fun set(address: SnesAddress, line: MetadataLine?): Metadata { + operator fun set(address: SnesAddress, line: MetadataLine?): GameData { if (line == null) { - code.remove(address) + metadata.remove(address) } else { - code[address] = line + metadata[address] = line } return this } operator fun get(address: SnesAddress): MetadataLine? { - return code[address] + return metadata[address] } - operator fun contains(address: SnesAddress) = code[address] != null + operator fun contains(address: SnesAddress) = metadata[address] != null fun getOrCreate(address: SnesAddress): MetadataLine { val line = this[address] @@ -49,7 +58,7 @@ class Metadata { } fun cleanUp() { - code.removeIf { _, v -> v.isEmpty() } + metadata.removeIf { _, v -> v.isEmpty() } } } diff --git a/src/main/java/com/smallhacker/disbrowser/game/GameSource.kt b/src/main/java/com/smallhacker/disbrowser/game/GameSource.kt new file mode 100644 index 0000000..d150762 --- /dev/null +++ b/src/main/java/com/smallhacker/disbrowser/game/GameSource.kt @@ -0,0 +1,51 @@ +package com.smallhacker.disbrowser.game + +import com.smallhacker.disbrowser.asm.SnesMemory +import com.smallhacker.disbrowser.util.jsonFile +import org.glassfish.jersey.server.ResourceConfig +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.ConcurrentHashMap +import javax.ws.rs.core.Configuration + +private val VALID_FILE_NAME = Regex("""^[a-zA-Z0-9_ ]*$""") + +class GameSource(private val gameDataDir: Path) { + private val gameCache = ConcurrentHashMap() + + fun getGame(name: String): Game? { + return gameCache.computeIfAbsent(name) { + loadGame(name, it) + } + } + + private fun loadGame(name: String, it: String): Game? { + if (!validFileName(name)) { + return null + } + + val gameDataFile = gameDataDir.resolve("$it.json").toFile() + if (!gameDataFile.exists()) { + return null + } + + return try { + val gameDataJsonFile = jsonFile(gameDataFile.toPath(), true) + + val gameData = gameDataJsonFile.load() + val gamePath = Paths.get(gameData.path) + val rom = SnesMemory.loadRom(gamePath) + Game(name, rom, gameData, gameDataJsonFile) + } catch (e: Exception) { + e.printStackTrace() + null + } + } + + private fun validFileName(name: String) = VALID_FILE_NAME.matches(name) +} + +private const val GAME_SOURCE_PROPERTY = "gameSource" + +fun ResourceConfig.addGameSource(path: Path) = this.property(GAME_SOURCE_PROPERTY, GameSource(path))!! +fun Configuration.getGameSource() = getProperty(GAME_SOURCE_PROPERTY)!! as GameSource \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt index bc805ca..1b29e83 100644 --- a/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt +++ b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt @@ -3,49 +3,67 @@ package com.smallhacker.disbrowser.resource import com.smallhacker.disbrowser.* import com.smallhacker.disbrowser.asm.SnesAddress import com.smallhacker.disbrowser.asm.VagueNumber +import com.smallhacker.disbrowser.game.GameSource +import com.smallhacker.disbrowser.game.getGameSource import java.nio.charset.StandardCharsets import javax.ws.rs.GET import javax.ws.rs.Path import javax.ws.rs.PathParam import javax.ws.rs.Produces +import javax.ws.rs.core.Configuration +import javax.ws.rs.core.Context import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response -@Path("/") +@Path("/{game}") class DisassemblyResource { + @Context + private lateinit var config: Configuration + private val games by lazy { config.getGameSource() } + @GET @Produces(MediaType.TEXT_HTML) - fun getIt() = handle { - //Service.showDisassemblyFromReset() - table { - Service.getVectors().forEach { - tr { - td { text(it.name) } - td { text("(" + it.vectorLocation.toFormattedString() + ")") } - td { - a { - text(it.label) - }.attr("href", "/${it.codeLocation.toSimpleString()}/MX") + fun getIt(@PathParam("game") gameName: String) = handle { + games.getGame(gameName)?.let { game -> + table { + Service.getVectors(game).forEach { + tr { + td { text(it.name) } + td { text("(" + it.vectorLocation.toFormattedString() + ")") } + td { + a { + text(it.label) + }.attr("href", "/${game.id}/${it.codeLocation.toSimpleString()}/MX") + } + td { text("(" + it.codeLocation.toFormattedString() + ")") } } - td { text("(" + it.codeLocation.toFormattedString() + ")") } } - } - }.addClass("vector-table") + }.addClass("vector-table") + } } @GET @Path("{address}") @Produces(MediaType.TEXT_HTML) - fun getIt(@PathParam("address") address: String) = getIt(address, "") + fun getIt( + @PathParam("game") gameName: String, + @PathParam("address") address: String + ) = getIt(gameName, address, "") @GET @Path("{address}/{state}") @Produces(MediaType.TEXT_HTML) - fun getIt(@PathParam("address") address: String, @PathParam("state") state: String): Response { + fun getIt( + @PathParam("game") gameName: String, + @PathParam("address") address: String, + @PathParam("state") state: String + ): Response { return handle { - SnesAddress.parse(address)?.let { - val flags = parseState(state) - Service.showDisassembly(it, flags) + games.getGame(gameName)?.let { game -> + SnesAddress.parse(address)?.let { + val flags = parseState(state) + Service.showDisassembly(game, it, flags) + } } } } diff --git a/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt index 6fc9ba8..6af8ad6 100644 --- a/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt +++ b/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt @@ -3,29 +3,46 @@ package com.smallhacker.disbrowser.resource import com.smallhacker.disbrowser.Service import com.smallhacker.disbrowser.asm.SnesAddress import com.smallhacker.disbrowser.asm.MetadataLine +import com.smallhacker.disbrowser.game.GameSource +import com.smallhacker.disbrowser.game.getGameSource import javax.ws.rs.Consumes import javax.ws.rs.POST import javax.ws.rs.Path import javax.ws.rs.PathParam +import javax.ws.rs.core.Configuration +import javax.ws.rs.core.Context import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response -@Path("/rest") +@Path("/rest/{game}") class RestResource { + @Context + private lateinit var config: Configuration + private val games by lazy { config.getGameSource() } + @POST @Path("{address}/{field}") @Consumes(MediaType.TEXT_PLAIN) - fun getIt(@PathParam("address") address: String, @PathParam("field") fieldName: String, body: String): Response { + fun getIt( + @PathParam("game") gameName: String, + @PathParam("address") address: String, + @PathParam("field") fieldName: String, + body: String + ): Response { val parsedAddress = SnesAddress.parse(address) ?: return Response.status(400).build() val field = when (fieldName) { "preComment" -> MetadataLine::preComment "comment" -> MetadataLine::comment "label" -> MetadataLine::label - else -> return Response.status(404).build() + else -> return http404() } - Service.updateMetadata(parsedAddress, field, body) + val game = games.getGame(gameName) + ?: return http404() + Service.updateMetadata(game, parsedAddress, field, body) return Response.noContent().build() } + + private fun http404(): Response = Response.status(404).build() } diff --git a/src/main/java/com/smallhacker/disbrowser/resource/StaticResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/StaticResource.kt index 04b095c..a255aa6 100644 --- a/src/main/java/com/smallhacker/disbrowser/resource/StaticResource.kt +++ b/src/main/java/com/smallhacker/disbrowser/resource/StaticResource.kt @@ -5,10 +5,10 @@ import javax.ws.rs.Path import javax.ws.rs.PathParam import javax.ws.rs.core.Response -@Path("/") +@Path("/resources") class StaticResource { @GET - @Path("resources/{file}.{ext}") + @Path("{file}.{ext}") fun getStatic(@PathParam("file") file: String, @PathParam("ext") ext: String): Response { val mime = when (ext) { "js" -> "application/javascript" diff --git a/src/main/ts/main.ts b/src/main/ts/main.ts index 8029345..10d376f 100644 --- a/src/main/ts/main.ts +++ b/src/main/ts/main.ts @@ -74,9 +74,10 @@ for (let i = 0; i < editables.length; i++) { let target = (e.target); let field = target.dataset.field || ""; let address = target.dataset.address; + let game = target.dataset.game; let value = (target).value; - xhr(`/rest/${address}/${field}`, "POST", value) + xhr(`/rest/${game}/${address}/${field}`, "POST", value) .catch((xhr: XMLHttpRequest) => alert("Error: HTTP " + xhr.status)); return false; @@ -94,12 +95,13 @@ for (let i = 0; i < popupEditables.length; i++) { let field = editable.dataset.field || ""; let address = editable.dataset.address; let value = editable.dataset.value; + let game = editable.dataset.game; let newValue = prompt("Label for $" + address, value); if (newValue === null || newValue == value) { return false; } - xhr(`/rest/${address}/${field}`, "POST", newValue) + xhr(`/rest/${game}/${address}/${field}`, "POST", newValue) .then(() => location.reload()) .catch((xhr: XMLHttpRequest) => alert("Error: HTTP " + xhr.status));