mirror of
https://github.com/Smallhacker/disbrowser.git
synced 2024-06-15 18:29:43 +00:00
Multi-game support, externalized config files
This commit is contained in:
parent
4c3cfea18a
commit
32b19c2c47
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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\"")
|
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
36
src/main/java/com/smallhacker/disbrowser/Main.kt
Normal file
36
src/main/java/com/smallhacker/disbrowser/Main.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
|
|
16
src/main/java/com/smallhacker/disbrowser/game/Game.kt
Normal file
16
src/main/java/com/smallhacker/disbrowser/game/Game.kt
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
51
src/main/java/com/smallhacker/disbrowser/game/GameSource.kt
Normal file
51
src/main/java/com/smallhacker/disbrowser/game/GameSource.kt
Normal 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
|
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user