Editing and persistence now works

This commit is contained in:
Smallhacker 2019-01-11 00:09:12 -05:00
parent 188d53a768
commit 32f3cdabff
12 changed files with 142 additions and 128 deletions

View File

@ -1,64 +1,39 @@
[ {
{ "008000" : {
"address": 32768, "label" : "ResetVector"
"label": "ResetVector",
"comment": null,
"flags": []
}, },
{ "008034" : {
"address": 32820, "label" : "MainGameLoop"
"label": "MainGameLoop",
"comment": null,
"flags": []
}, },
{ "0080b5" : {
"address": 32949, "label" : "JumpToGameMode"
"label": "JumpToGameMode",
"comment": null,
"flags": []
}, },
{ "0080c6" : {
"address": 165840, "flags" : [ {
"label": null, "flagType" : "JmpIndirectLongInterleavedTable",
"comment": null, "start" : "008061",
"flags": [ "entries" : 28
{ } ]
"flagType": "JslTableRoutine",
"entries": 4
}
]
}, },
{ "00841e" : {
"address": 32966, "label" : "ClearOam",
"label": null, "comment" : "Test3"
"comment": null,
"flags": [
{
"flagType": "JmpIndirectLongInterleavedTable",
"start": 32865,
"entries": 28
}
]
}, },
{ "00879c" : {
"address": 835861, "flags" : [ {
"label": null, "flagType" : "NonReturningRoutine"
"comment": null, } ]
"flags": [
{
"flagType": "JslTableRoutine",
"entries": 12
}
]
}, },
{ "0287d0" : {
"address": 34716, "flags" : [ {
"label": null, "flagType" : "JslTableRoutine",
"comment": null, "entries" : 4
"flags": [ } ]
{ },
"flagType": "NonReturningRoutine" "0cc115" : {
} "flags" : [ {
] "flagType" : "JslTableRoutine",
"entries" : 12
} ]
} }
] }

View File

@ -78,11 +78,7 @@ class Grid {
add(y, ins.address, add(y, ins.address,
text(actualAddress?.toFormattedString() ?: ""), text(actualAddress?.toFormattedString() ?: ""),
text(ins.bytesToString()), text(ins.bytesToString()),
input(value = insMetadata?.label ?: "") editableField(presentedAddress, "label", insMetadata?.label),
.addClass("field-label")
.addClass("field-editable")
.attr("data-field", "label")
.attr("data-address", presentedAddress.value.toString()),
fragment { fragment {
text(ins.printOpcodeAndSuffix()) text(ins.printOpcodeAndSuffix())
text(" ") text(" ")
@ -107,11 +103,7 @@ class Grid {
} }
}, },
text(ins.postState?.toString() ?: ""), text(ins.postState?.toString() ?: ""),
input(value = insMetadata?.comment ?: "") editableField(presentedAddress, "comment", insMetadata?.comment)
.addClass("field-comment")
.addClass("field-editable")
.attr("data-field", "comment")
.attr("data-address", presentedAddress.value.toString())
) )
if (ins.opcode.continuation == Continuation.NO) { if (ins.opcode.continuation == Continuation.NO) {
@ -119,6 +111,14 @@ class Grid {
} }
} }
private fun editableField(address: Address, type: String, value: String?): HtmlNode {
return input(value = value ?: "")
.addClass("field-$type")
.addClass("field-editable")
.attr("data-field", type)
.attr("data-address", address.toSimpleString())
}
private fun addDummy() { private fun addDummy() {
val y = (height++) val y = (height++)
add(y, null, null, null, null, text("..."), null, null) add(y, null, null, null, null, text("..."), null, null)

View File

@ -93,6 +93,8 @@ fun title(inner: InnerHtml = {}) = parent("title", inner)
fun HtmlArea.title(inner: InnerHtml = {}) = com.smallhacker.disbrowser.title(inner).appendTo(parent) fun HtmlArea.title(inner: InnerHtml = {}) = com.smallhacker.disbrowser.title(inner).appendTo(parent)
fun link(inner: InnerHtml = {}) = leaf("link") fun link(inner: InnerHtml = {}) = leaf("link")
fun HtmlArea.link(inner: InnerHtml = {}) = com.smallhacker.disbrowser.link(inner).appendTo(parent) fun HtmlArea.link(inner: InnerHtml = {}) = com.smallhacker.disbrowser.link(inner).appendTo(parent)
fun meta(inner: InnerHtml = {}) = leaf("meta")
fun HtmlArea.meta(inner: InnerHtml = {}) = com.smallhacker.disbrowser.meta(inner).appendTo(parent)
fun body(inner: InnerHtml = {}) = parent("body", inner) fun body(inner: InnerHtml = {}) = parent("body", inner)
fun HtmlArea.body(inner: InnerHtml = {}) = com.smallhacker.disbrowser.body(inner).appendTo(parent) fun HtmlArea.body(inner: InnerHtml = {}) = com.smallhacker.disbrowser.body(inner).appendTo(parent)
fun div(inner: InnerHtml = {}) = parent("div", inner) fun div(inner: InnerHtml = {}) = parent("div", inner)

View File

@ -13,7 +13,7 @@ object Service {
private val romName = "Zelda no Densetsu - Kamigami no Triforce (Japan)" private val romName = "Zelda no Densetsu - Kamigami no Triforce (Japan)"
private val romDir = Paths.get("""P:\Emulation\ROMs\SNES""") private val romDir = Paths.get("""P:\Emulation\ROMs\SNES""")
private val metaDir = Paths.get("""P:\Programming\dis-browser""") private val metaDir = Paths.get("""P:\Programming\dis-browser""")
private val metaFile = jsonFile<Metadata>(metaDir.resolve("$romName.json")) private val metaFile = jsonFile<Metadata>(metaDir.resolve("$romName.json"), true)
private val metadata by lazy { metaFile.load() } private val metadata by lazy { metaFile.load() }
private val romData = lazy { private val romData = lazy {
@ -57,6 +57,7 @@ object Service {
head { head {
title { text("Disassembly Browser") } title { text("Disassembly Browser") }
link {}.attr("rel", "stylesheet").attr("href", "/resources/style.css") link {}.attr("rel", "stylesheet").attr("href", "/resources/style.css")
meta {}.attr("charset", "UTF-8")
} }
body { body {
grid.output().appendTo(parent) grid.output().appendTo(parent)
@ -67,12 +68,23 @@ object Service {
fun updateMetadata(address: Address, field: KMutableProperty1<MetadataLine, String?>, value: String) { fun updateMetadata(address: Address, field: KMutableProperty1<MetadataLine, String?>, value: String) {
if (value.isEmpty()) { if (value.isEmpty()) {
metadata[address]?.run { if (address in metadata) {
field.set(this, null) doUpdateMetadata(address, field, null)
} }
} else { } else {
field.set(metadata.getOrAdd(address), value) doUpdateMetadata(address, field, value)
} }
} }
private fun doUpdateMetadata(address: Address, field: KMutableProperty1<MetadataLine, String?>, value: String?) {
val line = metadata.getOrCreate(address)
field.set(line, value)
if (line.isEmpty()) {
metadata[address] = null
}
metaFile.save(metadata)
}
} }

View File

@ -1,14 +1,15 @@
package com.smallhacker.disbrowser.asm package com.smallhacker.disbrowser.asm
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.annotation.JsonValue
import com.smallhacker.disbrowser.util.UInt24 import com.smallhacker.disbrowser.util.UInt24
import com.smallhacker.disbrowser.util.toUInt24 import com.smallhacker.disbrowser.util.toUInt24
import com.smallhacker.disbrowser.util.tryParseInt
data class Address(@JsonValue val value: UInt24): Comparable<Address> { data class Address(val value: UInt24) : Comparable<Address> {
@JsonIgnore
val rom = (value and 0x8000u).toUInt() == 0u val rom = (value and 0x8000u).toUInt() == 0u
@JsonIgnore
val pc = snesToPc(value) val pc = snesToPc(value)
operator fun plus(offset: Int) = Address(value + offset) operator fun plus(offset: Int) = Address(value + offset)
@ -18,13 +19,21 @@ data class Address(@JsonValue val value: UInt24): Comparable<Address> {
override fun toString(): String = toFormattedString() override fun toString(): String = toFormattedString()
fun toFormattedString(): String = String.format("$%02x:%04x", (value shr 16).toInt(), (value and 0xFFFFu).toInt()) fun toFormattedString(): String = String.format("$%02x:%04x", (value shr 16).toInt(), (value and 0xFFFFu).toInt())
@JsonValue
fun toSimpleString(): String = String.format("%06x", value.toInt()) fun toSimpleString(): String = String.format("%06x", value.toInt())
fun withinBank(value: UShort): Address = Address((this.value and 0xFF_0000u) or value.toUInt24()) fun withinBank(value: UShort): Address = Address((this.value and 0xFF_0000u) or value.toUInt24())
override fun compareTo(other: Address) = value.toUInt().compareTo(other.value.toUInt()) override fun compareTo(other: Address) = value.toUInt().compareTo(other.value.toUInt())
infix fun distanceTo(other: Address)= Math.abs(value.toInt() - other.value.toInt()).toUInt() infix fun distanceTo(other: Address) = Math.abs(value.toInt() - other.value.toInt()).toUInt()
companion object {
@JvmStatic
@JsonCreator
fun parse(address: String): Address? = tryParseInt(address, 16)
?.let { Address(it.toUInt24()) }
}
} }
fun address(snesAddress: Int) = Address(snesAddress.toUInt24()) fun address(snesAddress: Int) = Address(snesAddress.toUInt24())

View File

@ -4,20 +4,29 @@ import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.annotation.JsonSubTypes.Type import com.fasterxml.jackson.annotation.JsonSubTypes.Type
import com.smallhacker.disbrowser.util.joinBytes import com.smallhacker.disbrowser.util.joinBytes
import com.smallhacker.disbrowser.util.toUInt24 import com.smallhacker.disbrowser.util.toUInt24
import java.util.*
class Metadata() { class Metadata {
private val lines = HashMap<Address, MetadataLine>() private val lines: MutableMap<Address, MetadataLine>
@JsonValue constructor() {
private fun linesAsList() = lines.values.toList() this.lines = HashMap()
@JsonCreator
private constructor(@JsonProperty lines: List<MetadataLine>) : this() {
lines.forEach { add(it) }
} }
fun add(line: MetadataLine): Metadata { @JsonCreator
lines[line.address] = line private constructor(@JsonProperty lines: Map<Address, MetadataLine>) {
this.lines = HashMap(lines)
}
@JsonValue
private fun serialize() = TreeMap(lines)
operator fun set(address: Address, line: MetadataLine?): Metadata {
if (line == null) {
lines.remove(address)
} else {
lines[address] = line
}
return this return this
} }
@ -25,29 +34,19 @@ class Metadata() {
return lines[address] return lines[address]
} }
fun getOrAdd(address: Address): MetadataLine { operator fun contains(address: Address) = lines[address] != null
fun getOrCreate(address: Address): MetadataLine {
val line = this[address] val line = this[address]
if (line != null) { if (line != null) {
return line return line
} }
val newLine = MetadataLine(address) val newLine = MetadataLine()
add(newLine) this[address] = newLine
return newLine return newLine
} }
} }
fun Metadata.at(address: Int, runner: MetadataLine.() -> Unit) {
val line = MetadataLine(address(address))
this.add(line)
runner(line)
}
fun metadata(runner: Metadata.() -> Unit): Metadata {
val metadata = Metadata()
runner(metadata)
return metadata
}
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "flagType") @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "flagType")
@JsonSubTypes( @JsonSubTypes(
Type(value = NonReturningRoutine::class, name = "NonReturningRoutine"), Type(value = NonReturningRoutine::class, name = "NonReturningRoutine"),

View File

@ -1,9 +1,15 @@
package com.smallhacker.disbrowser.asm package com.smallhacker.disbrowser.asm
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonInclude
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
data class MetadataLine( data class MetadataLine(
val address: Address,
var label: String? = null, var label: String? = null,
var comment: String? = null, var comment: String? = null,
var preComment: String? = null, var preComment: String? = null,
val flags: MutableList<InstructionFlag> = ArrayList() val flags: MutableList<InstructionFlag> = ArrayList()
) ) {
@JsonIgnore
fun isEmpty() = (label == null) && (comment == null) && (preComment == null) && (flags.isEmpty())
}

View File

@ -4,8 +4,7 @@ import com.smallhacker.disbrowser.HtmlNode
import com.smallhacker.disbrowser.Service import com.smallhacker.disbrowser.Service
import com.smallhacker.disbrowser.asm.Address import com.smallhacker.disbrowser.asm.Address
import com.smallhacker.disbrowser.asm.VagueNumber import com.smallhacker.disbrowser.asm.VagueNumber
import com.smallhacker.disbrowser.util.toUInt24 import java.nio.charset.StandardCharsets
import com.smallhacker.disbrowser.util.tryParseInt
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
@ -31,18 +30,13 @@ class DisassemblyResource {
@Produces(MediaType.TEXT_HTML) @Produces(MediaType.TEXT_HTML)
fun getIt(@PathParam("address") address: String, @PathParam("state") state: String): Response { fun getIt(@PathParam("address") address: String, @PathParam("state") state: String): Response {
return handle { return handle {
parseAddress(address)?.let { Address.parse(address)?.let {
val flags = parseState(state) val flags = parseState(state)
Service.showDisassembly(it, flags) Service.showDisassembly(it, flags)
} }
} }
} }
private fun parseAddress(address: String): Address? {
return tryParseInt(address, 16)
?.let { Address(it.toUInt24()) }
}
private fun handle(runner: () -> HtmlNode?): Response { private fun handle(runner: () -> HtmlNode?): Response {
try { try {
val disassembly = runner() val disassembly = runner()
@ -50,7 +44,9 @@ class DisassemblyResource {
return if (disassembly == null) return if (disassembly == null)
Response.status(404).build() Response.status(404).build()
else else
Response.ok(disassembly.toString(), MediaType.TEXT_HTML).build() Response.ok(disassembly.toString().toByteArray(StandardCharsets.UTF_8))
.encoding("UTF-8")
.build()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
throw e throw e

View File

@ -3,8 +3,6 @@ package com.smallhacker.disbrowser.resource
import com.smallhacker.disbrowser.Service import com.smallhacker.disbrowser.Service
import com.smallhacker.disbrowser.asm.Address import com.smallhacker.disbrowser.asm.Address
import com.smallhacker.disbrowser.asm.MetadataLine import com.smallhacker.disbrowser.asm.MetadataLine
import com.smallhacker.disbrowser.util.toUInt24
import com.smallhacker.disbrowser.util.tryParseInt
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
@ -18,7 +16,7 @@ class RestResource {
@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("address") address: String, @PathParam("field") fieldName: String, body: String): Response {
val parsedAddress = parseAddress(address) ?: return Response.status(400).build() val parsedAddress = Address.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
@ -30,11 +28,4 @@ class RestResource {
return Response.noContent().build() return Response.noContent().build()
} }
private fun parseAddress(address: String): Address? {
return tryParseInt(address, 16)
?.let { Address(it.toUInt24()) }
}
} }

View File

@ -14,9 +14,10 @@ interface JsonFile<T> {
fun save(value: T) fun save(value: T)
} }
inline fun <reified T> jsonFile(path: Path): JsonFile<T> { inline fun <reified T> jsonFile(path: Path, prettyPrint: Boolean = false): JsonFile<T> {
val writer = if (prettyPrint) jsonMapper.writerWithDefaultPrettyPrinter() else jsonMapper.writer()
return object : JsonFile<T> { return object : JsonFile<T> {
override fun load() = jsonMapper.readValue<T>(path.toFile()) override fun load() = jsonMapper.readValue<T>(path.toFile())
override fun save(value: T) = jsonMapper.writeValue(path.toFile(), value) override fun save(value: T) = writer.writeValue(path.toFile(), value)
} }
} }

View File

@ -1,5 +1,5 @@
function center(el: HTMLElement) { function center(el: HTMLElement) {
function documentOffsetTop (el: HTMLElement) { function documentOffsetTop(el: HTMLElement) {
let top = el.offsetTop; let top = el.offsetTop;
let parent = el.offsetParent; let parent = el.offsetParent;
if (parent && parent instanceof HTMLElement) { if (parent && parent instanceof HTMLElement) {
@ -9,7 +9,7 @@ function center(el: HTMLElement) {
} }
let top = documentOffsetTop(el) - (window.innerHeight / 2); let top = documentOffsetTop(el) - (window.innerHeight / 2);
window.scrollTo( 0, top ); window.scrollTo(0, top);
} }
function highlight(el: HTMLElement | null) { function highlight(el: HTMLElement | null) {
@ -45,8 +45,27 @@ function fromUrl() {
return fromHash() || fromPath(); return fromHash() || fromPath();
} }
function xhr(url: string, method: string = "GET", body: (string | null) = null) {
return new Promise((resolve, reject) => {
let xhr = new XMLHttpRequest();
xhr.onload = () => {
if (xhr.status < 400) {
resolve(xhr);
} else {
reject(xhr);
}
};
xhr.onerror = () => reject(xhr);
xhr.onabort = () => reject(xhr);
xhr.open(method, url);
xhr.send(body);
});
}
highlight(fromUrl()); highlight(fromUrl());
window.addEventListener("hashchange", function () { highlight(fromHash()) }, false); window.addEventListener("hashchange", function () {
highlight(fromHash())
}, false);
let comments = document.getElementsByClassName("field-editable"); let comments = document.getElementsByClassName("field-editable");
for (let i = 0; i < comments.length; i++) { for (let i = 0; i < comments.length; i++) {
@ -54,9 +73,12 @@ for (let i = 0; i < comments.length; i++) {
comment.addEventListener("change", e => { comment.addEventListener("change", e => {
let target = <HTMLInputElement>(e.target); let target = <HTMLInputElement>(e.target);
let field = target.dataset.field || ""; let field = target.dataset.field || "";
let address = parseInt(target.dataset.address || "-1"); let address = target.dataset.address;
let value = (target).value; let value = (target).value;
alert(field + "/" + address + "=" + value);
xhr(`/rest/${address}/${field}`, "POST", value)
.catch((xhr: XMLHttpRequest) => alert("Error: HTTP " + xhr.status));
return false; return false;
}); });
} }

View File

@ -1,6 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"module": "amd", "module": "amd",
"target": "es2016",
"strictNullChecks": true, "strictNullChecks": true,
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitReturns": true, "noImplicitReturns": true,