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
import com.smallhacker.disbrowser.asm.*
import com.smallhacker.disbrowser.game.Game
import com.smallhacker.disbrowser.game.GameData
class Grid {
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 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())
}

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
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<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)
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<MetadataLine, String?>, value: String) {
fun updateMetadata(game: Game, address: SnesAddress, field: KMutableProperty1<MetadataLine, String?>, 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<MetadataLine, String?>, value: String?) {
val line = metadata.getOrCreate(address)
private fun doUpdateMetadata(game: Game, address: SnesAddress, field: KMutableProperty1<MetadataLine, String?>, 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)
}

View File

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

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: 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()

View File

@ -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(

View File

@ -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 {

View File

@ -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<UInt, UIntRange, MapperEntry> = 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
}

View File

@ -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<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 x: Boolean? get() = flags.getBoolean(0x10u)
val db: UByte? get() = pb // TODO

View File

@ -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<SnesAddress>()
val queue = ArrayDeque<State>()
@ -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())

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.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<SnesAddress, MetadataLine>
val name: String
@JsonProperty
val path: String
@JsonProperty
private val metadata: MutableMap<SnesAddress, MetadataLine>
constructor() {
this.code = TreeMap()
constructor(name: String, path: String) {
this.name = name
this.path = path
this.metadata = TreeMap()
}
@JsonCreator
private constructor(@JsonProperty code: Map<SnesAddress, MetadataLine>) {
this.code = TreeMap(code)
private constructor(@JsonProperty name: String, @JsonProperty path: String, @JsonProperty metadata: Map<SnesAddress, MetadataLine>) {
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() }
}
}

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.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)
}
}
}
}

View File

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

View File

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

View File

@ -74,9 +74,10 @@ for (let i = 0; i < editables.length; i++) {
let target = <HTMLInputElement>(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));