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

View File

@ -78,11 +78,7 @@ class Grid {
add(y, ins.address,
text(actualAddress?.toFormattedString() ?: ""),
text(ins.bytesToString()),
input(value = insMetadata?.label ?: "")
.addClass("field-label")
.addClass("field-editable")
.attr("data-field", "label")
.attr("data-address", presentedAddress.value.toString()),
editableField(presentedAddress, "label", insMetadata?.label),
fragment {
text(ins.printOpcodeAndSuffix())
text(" ")
@ -107,11 +103,7 @@ class Grid {
}
},
text(ins.postState?.toString() ?: ""),
input(value = insMetadata?.comment ?: "")
.addClass("field-comment")
.addClass("field-editable")
.attr("data-field", "comment")
.attr("data-address", presentedAddress.value.toString())
editableField(presentedAddress, "comment", insMetadata?.comment)
)
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() {
val y = (height++)
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 link(inner: InnerHtml = {}) = leaf("link")
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 HtmlArea.body(inner: InnerHtml = {}) = com.smallhacker.disbrowser.body(inner).appendTo(parent)
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 romDir = Paths.get("""P:\Emulation\ROMs\SNES""")
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 romData = lazy {
@ -57,6 +57,7 @@ object Service {
head {
title { text("Disassembly Browser") }
link {}.attr("rel", "stylesheet").attr("href", "/resources/style.css")
meta {}.attr("charset", "UTF-8")
}
body {
grid.output().appendTo(parent)
@ -67,12 +68,23 @@ object Service {
fun updateMetadata(address: Address, field: KMutableProperty1<MetadataLine, String?>, value: String) {
if (value.isEmpty()) {
metadata[address]?.run {
field.set(this, null)
if (address in metadata) {
doUpdateMetadata(address, field, null)
}
} 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
import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonValue
import com.smallhacker.disbrowser.util.UInt24
import com.smallhacker.disbrowser.util.toUInt24
import com.smallhacker.disbrowser.util.tryParseInt
data class Address(@JsonValue val value: UInt24): Comparable<Address> {
@JsonIgnore
data class Address(val value: UInt24) : Comparable<Address> {
val rom = (value and 0x8000u).toUInt() == 0u
@JsonIgnore
val pc = snesToPc(value)
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()
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 withinBank(value: UShort): Address = Address((this.value and 0xFF_0000u) or value.toUInt24())
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())

View File

@ -4,20 +4,29 @@ import com.fasterxml.jackson.annotation.*
import com.fasterxml.jackson.annotation.JsonSubTypes.Type
import com.smallhacker.disbrowser.util.joinBytes
import com.smallhacker.disbrowser.util.toUInt24
import java.util.*
class Metadata() {
private val lines = HashMap<Address, MetadataLine>()
class Metadata {
private val lines: MutableMap<Address, MetadataLine>
@JsonValue
private fun linesAsList() = lines.values.toList()
@JsonCreator
private constructor(@JsonProperty lines: List<MetadataLine>) : this() {
lines.forEach { add(it) }
constructor() {
this.lines = HashMap()
}
fun add(line: MetadataLine): Metadata {
lines[line.address] = line
@JsonCreator
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
}
@ -25,29 +34,19 @@ class Metadata() {
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]
if (line != null) {
return line
}
val newLine = MetadataLine(address)
add(newLine)
val newLine = MetadataLine()
this[address] = 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")
@JsonSubTypes(
Type(value = NonReturningRoutine::class, name = "NonReturningRoutine"),

View File

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

View File

@ -3,8 +3,6 @@ package com.smallhacker.disbrowser.resource
import com.smallhacker.disbrowser.Service
import com.smallhacker.disbrowser.asm.Address
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.POST
import javax.ws.rs.Path
@ -18,7 +16,7 @@ class RestResource {
@Path("{address}/{field}")
@Consumes(MediaType.TEXT_PLAIN)
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) {
"preComment" -> MetadataLine::preComment
"comment" -> MetadataLine::comment
@ -30,11 +28,4 @@ class RestResource {
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)
}
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> {
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 documentOffsetTop (el: HTMLElement) {
function documentOffsetTop(el: HTMLElement) {
let top = el.offsetTop;
let parent = el.offsetParent;
if (parent && parent instanceof HTMLElement) {
@ -9,7 +9,7 @@ function center(el: HTMLElement) {
}
let top = documentOffsetTop(el) - (window.innerHeight / 2);
window.scrollTo( 0, top );
window.scrollTo(0, top);
}
function highlight(el: HTMLElement | null) {
@ -45,8 +45,27 @@ function fromUrl() {
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());
window.addEventListener("hashchange", function () { highlight(fromHash()) }, false);
window.addEventListener("hashchange", function () {
highlight(fromHash())
}, false);
let comments = document.getElementsByClassName("field-editable");
for (let i = 0; i < comments.length; i++) {
@ -54,9 +73,12 @@ for (let i = 0; i < comments.length; i++) {
comment.addEventListener("change", e => {
let target = <HTMLInputElement>(e.target);
let field = target.dataset.field || "";
let address = parseInt(target.dataset.address || "-1");
let address = target.dataset.address;
let value = (target).value;
alert(field + "/" + address + "=" + value);
xhr(`/rest/${address}/${field}`, "POST", value)
.catch((xhr: XMLHttpRequest) => alert("Error: HTTP " + xhr.status));
return false;
});
}

View File

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