Multi-game support, externalized config files

This commit is contained in:
Smallhacker 2019-01-13 20:57:05 -05:00
parent 4c3cfea18a
commit 32b19c2c47
20 changed files with 284 additions and 403 deletions

View File

@ -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"
}
}
}

View File

@ -1,6 +1,8 @@
package com.smallhacker.disbrowser package com.smallhacker.disbrowser
import com.smallhacker.disbrowser.asm.* import com.smallhacker.disbrowser.asm.*
import com.smallhacker.disbrowser.game.Game
import com.smallhacker.disbrowser.game.GameData
class Grid { class Grid {
private val arrowCells = HashMap<Pair<Int, Int>, HtmlNode?>() private val arrowCells = HashMap<Pair<Int, Int>, HtmlNode?>()
@ -45,7 +47,6 @@ class Grid {
arrowClasses[x to y] = "arrow arrow-$dir-middle" arrowClasses[x to y] = "arrow arrow-$dir-middle"
} }
arrowClasses[x to y2] = "arrow arrow-$dir-end" 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") arrowCells[x to yEnd] = div().addClass("arrow-head")
} }
@ -59,8 +60,9 @@ class Grid {
.first() .first()
} }
fun add(ins: CodeUnit, metadata: Metadata, disassembly: Disassembly) { fun add(ins: CodeUnit, game: Game, disassembly: Disassembly) {
val presentedAddress = ins.presentedAddress val presentedAddress = ins.presentedAddress
val gameData = game.gameData
if (nextAddress != null) { if (nextAddress != null) {
if (presentedAddress != nextAddress) { if (presentedAddress != nextAddress) {
@ -73,12 +75,12 @@ class Grid {
addresses[presentedAddress] = y addresses[presentedAddress] = y
val (address, bytes, label, primaryMnemonic, secondaryMnemonic, suffix, operands, state, comment, labelAddress) val (address, bytes, label, primaryMnemonic, secondaryMnemonic, suffix, operands, state, comment, labelAddress)
= ins.print(metadata) = ins.print(gameData)
add(y, ins.address, add(y, ins.address,
text(address ?: ""), text(address ?: ""),
text(bytes), text(bytes),
editableField(presentedAddress, "label", label), editableField(game, presentedAddress, "label", label),
fragment { fragment {
if (secondaryMnemonic == null) { if (secondaryMnemonic == null) {
text(primaryMnemonic) text(primaryMnemonic)
@ -94,26 +96,24 @@ class Grid {
if (labelAddress == null) { if (labelAddress == null) {
text(operands ?: "") text(operands ?: "")
} else { } else {
val currentLabel = metadata[labelAddress]?.label val currentLabel = gameData[labelAddress]?.label
editablePopupField(labelAddress, "label", operands, currentLabel) editablePopupField(game, labelAddress, "label", operands, currentLabel)
} }
} else { } else {
val local = link.address in disassembly val local = link.address in disassembly
val url = when { val url = when {
local -> "#${link.address.toSimpleString()}" 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 { a {
text(operands ?: "") text(operands ?: "")
}.attr("href", url) }.attr("href", url)
} }
}, },
text(state ?: ""), text(state ?: ""),
editableField(presentedAddress, "comment", comment) editableField(game, presentedAddress, "comment", comment)
) )
if (ins.opcode.continuation == Continuation.NO) { 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 ?: "") return input(value = value ?: "")
.addClass("field-$type") .addClass("field-$type")
.addClass("field-editable") .addClass("field-editable")
.attr("data-field", type) .attr("data-field", type)
.attr("data-game", game.id)
.attr("data-address", address.toSimpleString()) .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 { span {
text(displayValue ?: "") text(displayValue ?: "")
span {}.addClass("field-editable-popup-icon") span {}.addClass("field-editable-popup-icon")
@ -138,6 +139,7 @@ class Grid {
.addClass("field-editable-popup") .addClass("field-editable-popup")
.attr("data-field", type) .attr("data-field", type)
.attr("data-value", editValue ?: "") .attr("data-value", editValue ?: "")
.attr("data-game", game.id)
.attr("data-address", address.toSimpleString()) .attr("data-address", address.toSimpleString())
} }

View File

@ -1,39 +0,0 @@
//package com.smallhacker.disbrowser
//
//class HtmlContext(val out: StringBuilder)
//
//fun html(inner: HtmlContext.() -> Unit = {}): String {
// val html = HtmlContext(StringBuilder().append("<!DOCTYPE html>"))
// 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("</$tag>")
//}
//
//
//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\"")

View File

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

View File

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

View File

@ -1,10 +1,10 @@
package com.smallhacker.disbrowser package com.smallhacker.disbrowser
import com.smallhacker.disbrowser.asm.* import com.smallhacker.disbrowser.asm.*
import com.smallhacker.disbrowser.game.Game
import com.smallhacker.disbrowser.disassembler.Disassembler 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 com.smallhacker.disbrowser.util.toUInt24
import java.nio.file.Paths
import kotlin.reflect.KMutableProperty1 import kotlin.reflect.KMutableProperty1
private val RESET_VECTOR_LOCATION = address(0x00_FFFC) private val RESET_VECTOR_LOCATION = address(0x00_FFFC)
@ -25,37 +25,26 @@ private val VECTORS = listOf(
) )
object Service { object Service {
private const val romName = "Zelda no Densetsu - Kamigami no Triforce (Japan)" fun showDisassemblyFromReset(game: Game): HtmlNode? {
private val romDir = Paths.get("""P:\Emulation\ROMs\SNES""") val resetVector = game.memory.getWord(RESET_VECTOR_LOCATION)
private val metaDir = Paths.get("""P:\Programming\disbrowser""")
private val metaFile = jsonFile<Metadata>(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)
val fullResetVector = resetVector!!.toUInt24() val fullResetVector = resetVector!!.toUInt24()
val initialAddress = SnesAddress(fullResetVector) val initialAddress = SnesAddress(fullResetVector)
val flags = VagueNumber(0x30u) val flags = VagueNumber(0x30u)
return showDisassembly(initialAddress, flags) return showDisassembly(game, initialAddress, flags)
} }
fun showDisassembly(initialAddress: SnesAddress, flags: VagueNumber): HtmlNode? { fun showDisassembly(game: Game, initialAddress: SnesAddress, flags: VagueNumber): HtmlNode? {
val initialState = State(memory = snesMemory, address = initialAddress, flags = flags, metadata = metadata) val initialState = State(memory = game.memory, address = initialAddress, flags = flags, gameData = game.gameData)
val disassembly = Disassembler.disassemble(initialState, metadata, false) 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() val grid = Grid()
disassembly.forEach { disassembly.forEach {
grid.add(it, metadata, disassembly) grid.add(it, game, disassembly)
} }
disassembly.asSequence() disassembly.asSequence()
.mapNotNull { .mapNotNull {
@ -70,28 +59,27 @@ object Service {
return grid.output() return grid.output()
} }
fun updateMetadata(address: SnesAddress, field: KMutableProperty1<MetadataLine, String?>, value: String) { fun updateMetadata(game: Game, address: SnesAddress, field: KMutableProperty1<MetadataLine, String?>, value: String) {
if (value.isEmpty()) { if (value.isEmpty()) {
if (address in metadata) { if (address in game.gameData) {
doUpdateMetadata(address, field, null) doUpdateMetadata(game, address, field, null)
} }
} else { } else {
doUpdateMetadata(address, field, value) doUpdateMetadata(game, address, field, value)
} }
} }
private fun doUpdateMetadata(address: SnesAddress, field: KMutableProperty1<MetadataLine, String?>, value: String?) { private fun doUpdateMetadata(game: Game, address: SnesAddress, field: KMutableProperty1<MetadataLine, String?>, value: String?) {
val line = metadata.getOrCreate(address) val line = game.gameData.getOrCreate(address)
field.set(line, value) field.set(line, value)
metadata.cleanUp() game.saveGameData()
metaFile.save(metadata)
} }
fun getVectors() = VECTORS.asSequence() fun getVectors(game: Game) = VECTORS.asSequence()
.map { (vectorLocation: SnesAddress, name: String ) -> .map { (vectorLocation: SnesAddress, name: String ) ->
val codeLocation = SnesAddress(snesMemory.getWord(vectorLocation)!!.toUInt24()) val codeLocation = SnesAddress(game.memory.getWord(vectorLocation)!!.toUInt24())
val label = metadata[codeLocation]?.label val label = game.gameData[codeLocation]?.label
?: codeLocation.toFormattedString() ?: codeLocation.toFormattedString()
Vector(vectorLocation, codeLocation, name, label) Vector(vectorLocation, codeLocation, name, label)
} }

View File

@ -1,5 +1,6 @@
package com.smallhacker.disbrowser.asm package com.smallhacker.disbrowser.asm
import com.smallhacker.disbrowser.game.GameData
import com.smallhacker.disbrowser.util.* import com.smallhacker.disbrowser.util.*
interface CodeUnit { interface CodeUnit {
@ -16,7 +17,7 @@ interface CodeUnit {
val opcode: Opcode val opcode: Opcode
val lengthSuffix: String? val lengthSuffix: String?
val memory: SnesMapper val memory: SnesMemory
fun operandByte(index: UInt): UByte = bytes[opcode.operandIndex + index] fun operandByte(index: UInt): UByte = bytes[opcode.operandIndex + index]
@ -57,7 +58,7 @@ class DataBlock(
override val presentedAddress: SnesAddress, override val presentedAddress: SnesAddress,
override val relativeAddress: SnesAddress, override val relativeAddress: SnesAddress,
override val linkedState: State?, override val linkedState: State?,
override val memory: SnesMapper override val memory: SnesMemory
) : CodeUnit { ) : CodeUnit {
override val nextPresentedAddress: SnesAddress override val nextPresentedAddress: SnesAddress
get() = presentedAddress + operandLength.toInt() 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 mnemonic = opcode.mnemonic
val primaryMnemonic = mnemonic.displayName val primaryMnemonic = mnemonic.displayName
val secondaryMnemonic = mnemonic.alternativeName val secondaryMnemonic = mnemonic.alternativeName
var suffix = lengthSuffix var suffix = lengthSuffix
var operands = metadata?.let { opcode.mode.printWithLabel(this, it) } var operands = gameData?.let { opcode.mode.printWithLabel(this, it) }
if (operands == null) { if (operands == null) {
operands = opcode.mode.printRaw(this) operands = opcode.mode.printRaw(this)
suffix = null suffix = null
} }
val state = postState?.toString() val state = postState?.toString()
val label = address?.let { metadata?.get(it)?.label } val label = address?.let { gameData?.get(it)?.label }
val comment = address?.let { metadata?.get(it)?.comment } val comment = address?.let { gameData?.get(it)?.comment }
val formattedAddress = address?.toFormattedString() val formattedAddress = address?.toFormattedString()
val bytes = bytesToString() val bytes = bytesToString()

View File

@ -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: UInt, length: UInt): ValidMemorySpace = MemoryRange(this, start, length).validate()!!
fun ValidMemorySpace.range(start: SnesAddress, length: UInt): ValidMemorySpace = range(start.value.toUInt(), 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 { class ArrayMemorySpace(private val bytes: UByteArray) : MemorySpace {
override val size = bytes.size.toUInt() override val size = bytes.size.toUInt()

View File

@ -2,6 +2,7 @@ package com.smallhacker.disbrowser.asm
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude import com.fasterxml.jackson.annotation.JsonInclude
import com.smallhacker.disbrowser.game.InstructionFlag
@JsonInclude(JsonInclude.Include.NON_DEFAULT) @JsonInclude(JsonInclude.Include.NON_DEFAULT)
data class MetadataLine( data class MetadataLine(

View File

@ -1,5 +1,6 @@
package com.smallhacker.disbrowser.asm package com.smallhacker.disbrowser.asm
import com.smallhacker.disbrowser.game.GameData
import com.smallhacker.disbrowser.util.* import com.smallhacker.disbrowser.util.*
interface Mode { interface Mode {
@ -7,7 +8,7 @@ interface Mode {
val showLengthSuffix get() = true val showLengthSuffix get() = true
val canHaveLabel: Boolean val canHaveLabel: Boolean
fun operandLength(state: State): UInt? 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 printRaw(ins: CodeUnit): String
fun referencedAddress(ins: CodeUnit): SnesAddress? fun referencedAddress(ins: CodeUnit): SnesAddress?
} }
@ -30,7 +31,7 @@ private abstract class RawWrappedMode(
private val suffix: String = "" private val suffix: String = ""
) : Mode by parent { ) : Mode by parent {
override val canHaveLabel = false 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 override fun printRaw(ins: CodeUnit) = prefix + parent.printRaw(ins) + suffix
} }
@ -39,7 +40,7 @@ private abstract class WrappedMode(
private val prefix: String = "", private val prefix: String = "",
private val suffix: String = "" private val suffix: String = ""
) : Mode by parent { ) : 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 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 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) } 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 { private interface DataValueType {
fun resolve(byte: UByte, state: State?, memory: SnesMapper): SnesAddress? fun resolve(byte: UByte, state: State?, memory: SnesMemory): SnesAddress?
fun resolve(word: UShort, state: State?, memory: SnesMapper): SnesAddress? fun resolve(word: UShort, state: State?, memory: SnesMemory): SnesAddress?
fun resolve(long: UInt24, state: State?, memory: SnesMapper): SnesAddress? fun resolve(long: UInt24, state: State?, memory: SnesMemory): SnesAddress?
val canHaveLabel: Boolean val canHaveLabel: Boolean
val byte: Mode get() = DataByteMode(this) val byte: Mode get() = DataByteMode(this)
@ -85,23 +86,23 @@ private interface DataValueType {
object DirectData : DataValueType { object DirectData : DataValueType {
override val canHaveLabel = false override val canHaveLabel = false
override fun resolve(byte: UByte, 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: SnesMapper): SnesAddress? = null override fun resolve(word: UShort, state: State?, memory: SnesMemory): SnesAddress? = null
override fun resolve(long: UInt24, state: State?, memory: SnesMapper): SnesAddress? = null override fun resolve(long: UInt24, state: State?, memory: SnesMemory): SnesAddress? = null
} }
object DataPointer : DataValueType { object DataPointer : DataValueType {
override val canHaveLabel = true override val canHaveLabel = true
override fun resolve(byte: UByte, state: State?, memory: SnesMapper) = state?.resolveDirectPage(byte) override fun resolve(byte: UByte, state: State?, memory: SnesMemory) = state?.resolveDirectPage(byte)
override fun resolve(word: UShort, state: State?, memory: SnesMapper) = state?.resolveAbsoluteData(word) override fun resolve(word: UShort, state: State?, memory: SnesMemory) = state?.resolveAbsoluteData(word)
override fun resolve(long: UInt24, state: State?, memory: SnesMapper) = memory.toCanonical(SnesAddress(long)) override fun resolve(long: UInt24, state: State?, memory: SnesMemory) = memory.toCanonical(SnesAddress(long))
} }
object CodePointer : DataValueType { object CodePointer : DataValueType {
override val canHaveLabel = true override val canHaveLabel = true
override fun resolve(byte: UByte, state: State?, memory: SnesMapper) = state?.resolveDirectPage(byte) override fun resolve(byte: UByte, state: State?, memory: SnesMemory) = state?.resolveDirectPage(byte)
override fun resolve(word: UShort, state: State?, memory: SnesMapper) = state?.resolveAbsoluteCode(word) override fun resolve(word: UShort, state: State?, memory: SnesMemory) = state?.resolveAbsoluteCode(word)
override fun resolve(long: UInt24, state: State?, memory: SnesMapper) = memory.toCanonical(SnesAddress(long)) 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 { private abstract class DataValueMode(private val length: UInt, protected val type: DataValueType) : Mode {

View File

@ -3,8 +3,10 @@ package com.smallhacker.disbrowser.asm
import com.smallhacker.disbrowser.datatype.MutableRangeMap import com.smallhacker.disbrowser.datatype.MutableRangeMap
import com.smallhacker.disbrowser.datatype.NaiveRangeMap import com.smallhacker.disbrowser.datatype.NaiveRangeMap
import com.smallhacker.disbrowser.util.toUInt24 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 override val size = 0x100_0000u
private val areas: MutableRangeMap<UInt, UIntRange, MapperEntry> = NaiveRangeMap() private val areas: MutableRangeMap<UInt, UIntRange, MapperEntry> = NaiveRangeMap()
@ -24,6 +26,15 @@ abstract class SnesMapper: MemorySpace {
val offset = address.value - entry.start val offset = address.value - entry.start
return entry.canonicalStart + offset.toInt() 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()) 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) data class MapperEntry(val start: UInt, val canonicalStart: SnesAddress, val space: MemorySpace)
class SnesLoRom(romData: MemorySpace): SnesMapper() { class SnesLoRom(romData: MemorySpace): SnesMemory() {
init { init {
val ram = UnreadableMemory(0x2_0000u) val ram = UnreadableMemory(0x2_0000u)
val ramMirror = ram.range(0x00_0000u, 0x00_2000u) val ramMirror = ram.range(0x00_0000u, 0x00_2000u)
@ -58,10 +69,18 @@ class SnesLoRom(romData: MemorySpace): SnesMapper() {
} }
for (bank in 0x40u..0x6Fu) { for (bank in 0x40u..0x6Fu) {
val romArea = (bank shl 16) or 0x00_8000u val lowerRomArea = (bank shl 16)
// Lower half unmapped val upperRomArea = (bank shl 16) or 0x00_8000u
add(romArea, romArea, romData.range(pc, 0x00_8000u)) // Mirror upper and lower banks to the same ROM space.
add(romArea + high, romArea, romData.range(pc, 0x00_8000u)) // 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 pc += 0x00_8000u
} }

View File

@ -1,10 +1,11 @@
package com.smallhacker.disbrowser.asm package com.smallhacker.disbrowser.asm
import com.smallhacker.disbrowser.ImmStack import com.smallhacker.disbrowser.ImmStack
import com.smallhacker.disbrowser.game.GameData
import com.smallhacker.disbrowser.immStack import com.smallhacker.disbrowser.immStack
import com.smallhacker.disbrowser.util.toUInt24 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<VagueNumber> = immStack(), val metadata: Metadata) { data class State(val origin: Instruction? = null, val memory: SnesMemory, val address: SnesAddress, val flags: VagueNumber = VagueNumber(), val stack: ImmStack<VagueNumber> = immStack(), val gameData: GameData) {
val m: Boolean? get() = flags.getBoolean(0x20u) val m: Boolean? get() = flags.getBoolean(0x20u)
val x: Boolean? get() = flags.getBoolean(0x10u) val x: Boolean? get() = flags.getBoolean(0x10u)
val db: UByte? get() = pb // TODO val db: UByte? get() = pb // TODO

View File

@ -1,11 +1,15 @@
package com.smallhacker.disbrowser.disassembler package com.smallhacker.disbrowser.disassembler
import com.smallhacker.disbrowser.asm.* 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 java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
object Disassembler { object Disassembler {
fun disassemble(initialState: State, metadata: Metadata, global: Boolean): Disassembly { fun disassemble(initialState: State, gameData: GameData, global: Boolean): Disassembly {
val seen = HashSet<SnesAddress>() val seen = HashSet<SnesAddress>()
val queue = ArrayDeque<State>() val queue = ArrayDeque<State>()
@ -26,7 +30,7 @@ object Disassembler {
var stop = (ins.opcode.continuation == Continuation.NO) or var stop = (ins.opcode.continuation == Continuation.NO) or
(ins.opcode.mode.instructionLength(state) == null) (ins.opcode.mode.instructionLength(state) == null)
metadata[ins.address]?.flags?.forEach { flag -> gameData[ins.address]?.flags?.forEach { flag ->
if (flag is JmpIndirectLongInterleavedTable) { if (flag is JmpIndirectLongInterleavedTable) {
if (global) { if (global) {
flag.readTable(state.memory) flag.readTable(state.memory)
@ -53,7 +57,7 @@ object Disassembler {
val linkedState = ins.linkedState val linkedState = ins.linkedState
if (linkedState != null) { if (linkedState != null) {
metadata[linkedState.address]?.flags?.forEach { gameData[linkedState.address]?.flags?.forEach {
if (it === NonReturningRoutine) { if (it === NonReturningRoutine) {
stop = true stop = true
println(ins.address.toFormattedString()) println(ins.address.toFormattedString())

View File

@ -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<GameData>
) {
fun saveGameData() {
gameData.cleanUp()
gameDataFile.save(gameData)
}
}

View File

@ -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.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonSubTypes.Type import com.fasterxml.jackson.annotation.JsonSubTypes.Type
import com.fasterxml.jackson.annotation.JsonTypeInfo import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.smallhacker.disbrowser.asm.*
import com.smallhacker.disbrowser.util.joinNullableBytes import com.smallhacker.disbrowser.util.joinNullableBytes
import com.smallhacker.disbrowser.util.removeIf import com.smallhacker.disbrowser.util.removeIf
import com.smallhacker.disbrowser.util.toUInt24 import com.smallhacker.disbrowser.util.toUInt24
import java.util.* import java.util.*
class Metadata { class GameData {
@JsonProperty @JsonProperty
private val code: MutableMap<SnesAddress, MetadataLine> val name: String
@JsonProperty
val path: String
@JsonProperty
private val metadata: MutableMap<SnesAddress, MetadataLine>
constructor() { constructor(name: String, path: String) {
this.code = TreeMap() this.name = name
this.path = path
this.metadata = TreeMap()
} }
@JsonCreator @JsonCreator
private constructor(@JsonProperty code: Map<SnesAddress, MetadataLine>) { private constructor(@JsonProperty name: String, @JsonProperty path: String, @JsonProperty metadata: Map<SnesAddress, MetadataLine>) {
this.code = TreeMap(code) 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) { if (line == null) {
code.remove(address) metadata.remove(address)
} else { } else {
code[address] = line metadata[address] = line
} }
return this return this
} }
operator fun get(address: SnesAddress): MetadataLine? { 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 { fun getOrCreate(address: SnesAddress): MetadataLine {
val line = this[address] val line = this[address]
@ -49,7 +58,7 @@ class Metadata {
} }
fun cleanUp() { fun cleanUp() {
code.removeIf { _, v -> v.isEmpty() } metadata.removeIf { _, v -> v.isEmpty() }
} }
} }

View File

@ -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<String, Game?>()
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<GameData>(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

View File

@ -3,49 +3,67 @@ package com.smallhacker.disbrowser.resource
import com.smallhacker.disbrowser.* import com.smallhacker.disbrowser.*
import com.smallhacker.disbrowser.asm.SnesAddress import com.smallhacker.disbrowser.asm.SnesAddress
import com.smallhacker.disbrowser.asm.VagueNumber import com.smallhacker.disbrowser.asm.VagueNumber
import com.smallhacker.disbrowser.game.GameSource
import com.smallhacker.disbrowser.game.getGameSource
import java.nio.charset.StandardCharsets import java.nio.charset.StandardCharsets
import javax.ws.rs.GET import javax.ws.rs.GET
import javax.ws.rs.Path import javax.ws.rs.Path
import javax.ws.rs.PathParam import javax.ws.rs.PathParam
import javax.ws.rs.Produces 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.MediaType
import javax.ws.rs.core.Response import javax.ws.rs.core.Response
@Path("/") @Path("/{game}")
class DisassemblyResource { class DisassemblyResource {
@Context
private lateinit var config: Configuration
private val games by lazy { config.getGameSource() }
@GET @GET
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
fun getIt() = handle { fun getIt(@PathParam("game") gameName: String) = handle {
//Service.showDisassemblyFromReset() games.getGame(gameName)?.let { game ->
table { table {
Service.getVectors().forEach { Service.getVectors(game).forEach {
tr { tr {
td { text(it.name) } td { text(it.name) }
td { text("(" + it.vectorLocation.toFormattedString() + ")") } td { text("(" + it.vectorLocation.toFormattedString() + ")") }
td { td {
a { a {
text(it.label) text(it.label)
}.attr("href", "/${it.codeLocation.toSimpleString()}/MX") }.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 @GET
@Path("{address}") @Path("{address}")
@Produces(MediaType.TEXT_HTML) @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 @GET
@Path("{address}/{state}") @Path("{address}/{state}")
@Produces(MediaType.TEXT_HTML) @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 { return handle {
SnesAddress.parse(address)?.let { games.getGame(gameName)?.let { game ->
val flags = parseState(state) SnesAddress.parse(address)?.let {
Service.showDisassembly(it, flags) val flags = parseState(state)
Service.showDisassembly(game, it, flags)
}
} }
} }
} }

View File

@ -3,29 +3,46 @@ package com.smallhacker.disbrowser.resource
import com.smallhacker.disbrowser.Service import com.smallhacker.disbrowser.Service
import com.smallhacker.disbrowser.asm.SnesAddress import com.smallhacker.disbrowser.asm.SnesAddress
import com.smallhacker.disbrowser.asm.MetadataLine 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.Consumes
import javax.ws.rs.POST import javax.ws.rs.POST
import javax.ws.rs.Path import javax.ws.rs.Path
import javax.ws.rs.PathParam 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.MediaType
import javax.ws.rs.core.Response import javax.ws.rs.core.Response
@Path("/rest") @Path("/rest/{game}")
class RestResource { class RestResource {
@Context
private lateinit var config: Configuration
private val games by lazy { config.getGameSource() }
@POST @POST
@Path("{address}/{field}") @Path("{address}/{field}")
@Consumes(MediaType.TEXT_PLAIN) @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 parsedAddress = SnesAddress.parse(address) ?: return Response.status(400).build()
val field = when (fieldName) { val field = when (fieldName) {
"preComment" -> MetadataLine::preComment "preComment" -> MetadataLine::preComment
"comment" -> MetadataLine::comment "comment" -> MetadataLine::comment
"label" -> MetadataLine::label "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() return Response.noContent().build()
} }
private fun http404(): Response = Response.status(404).build()
} }

View File

@ -5,10 +5,10 @@ import javax.ws.rs.Path
import javax.ws.rs.PathParam import javax.ws.rs.PathParam
import javax.ws.rs.core.Response import javax.ws.rs.core.Response
@Path("/") @Path("/resources")
class StaticResource { class StaticResource {
@GET @GET
@Path("resources/{file}.{ext}") @Path("{file}.{ext}")
fun getStatic(@PathParam("file") file: String, @PathParam("ext") ext: String): Response { fun getStatic(@PathParam("file") file: String, @PathParam("ext") ext: String): Response {
val mime = when (ext) { val mime = when (ext) {
"js" -> "application/javascript" "js" -> "application/javascript"

View File

@ -74,9 +74,10 @@ for (let i = 0; i < editables.length; i++) {
let target = <HTMLInputElement>(e.target); let target = <HTMLInputElement>(e.target);
let field = target.dataset.field || ""; let field = target.dataset.field || "";
let address = target.dataset.address; let address = target.dataset.address;
let game = target.dataset.game;
let value = (target).value; 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)); .catch((xhr: XMLHttpRequest) => alert("Error: HTTP " + xhr.status));
return false; return false;
@ -94,12 +95,13 @@ for (let i = 0; i < popupEditables.length; i++) {
let field = editable.dataset.field || ""; let field = editable.dataset.field || "";
let address = editable.dataset.address; let address = editable.dataset.address;
let value = editable.dataset.value; let value = editable.dataset.value;
let game = editable.dataset.game;
let newValue = prompt("Label for $" + address, value); let newValue = prompt("Label for $" + address, value);
if (newValue === null || newValue == value) { if (newValue === null || newValue == value) {
return false; return false;
} }
xhr(`/rest/${address}/${field}`, "POST", newValue) xhr(`/rest/${game}/${address}/${field}`, "POST", newValue)
.then(() => location.reload()) .then(() => location.reload())
.catch((xhr: XMLHttpRequest) => alert("Error: HTTP " + xhr.status)); .catch((xhr: XMLHttpRequest) => alert("Error: HTTP " + xhr.status));