diff --git a/Zelda no Densetsu - Kamigami no Triforce (Japan).json b/Zelda no Densetsu - Kamigami no Triforce (Japan).json index 536e3ec..cace8fe 100644 --- a/Zelda no Densetsu - Kamigami no Triforce (Japan).json +++ b/Zelda no Densetsu - Kamigami no Triforce (Japan).json @@ -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 + } ] } -] \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/java/com/smallhacker/disbrowser/Grid.kt b/src/main/java/com/smallhacker/disbrowser/Grid.kt index b4600ec..77fd4ac 100644 --- a/src/main/java/com/smallhacker/disbrowser/Grid.kt +++ b/src/main/java/com/smallhacker/disbrowser/Grid.kt @@ -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) diff --git a/src/main/java/com/smallhacker/disbrowser/Html.kt b/src/main/java/com/smallhacker/disbrowser/Html.kt index 3986e2b..f42bd06 100644 --- a/src/main/java/com/smallhacker/disbrowser/Html.kt +++ b/src/main/java/com/smallhacker/disbrowser/Html.kt @@ -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) diff --git a/src/main/java/com/smallhacker/disbrowser/Service.kt b/src/main/java/com/smallhacker/disbrowser/Service.kt index bade567..8027900 100644 --- a/src/main/java/com/smallhacker/disbrowser/Service.kt +++ b/src/main/java/com/smallhacker/disbrowser/Service.kt @@ -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(metaDir.resolve("$romName.json")) + private val metaFile = jsonFile(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, 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, value: String?) { + val line = metadata.getOrCreate(address) + field.set(line, value) + + if (line.isEmpty()) { + metadata[address] = null + } + + metaFile.save(metadata) + } + } diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Address.kt b/src/main/java/com/smallhacker/disbrowser/asm/Address.kt index cf37600..df37416 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Address.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Address.kt @@ -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
{ - @JsonIgnore +data class Address(val value: UInt24) : Comparable
{ 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
{ 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()) diff --git a/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt b/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt index f1bc10c..ea20d11 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/Metadata.kt @@ -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() +class Metadata { + private val lines: MutableMap - @JsonValue - private fun linesAsList() = lines.values.toList() - - @JsonCreator - private constructor(@JsonProperty lines: List) : this() { - lines.forEach { add(it) } + constructor() { + this.lines = HashMap() } - fun add(line: MetadataLine): Metadata { - lines[line.address] = line + @JsonCreator + private constructor(@JsonProperty lines: Map) { + 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"), diff --git a/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt b/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt index 0e21225..e4d13a2 100644 --- a/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt +++ b/src/main/java/com/smallhacker/disbrowser/asm/MetadataLine.kt @@ -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 = ArrayList() -) +) { + @JsonIgnore + fun isEmpty() = (label == null) && (comment == null) && (preComment == null) && (flags.isEmpty()) +} diff --git a/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt index b580484..3b0d10a 100644 --- a/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt +++ b/src/main/java/com/smallhacker/disbrowser/resource/DisassemblyResource.kt @@ -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 diff --git a/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt b/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt index ca1d34f..86df856 100644 --- a/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt +++ b/src/main/java/com/smallhacker/disbrowser/resource/RestResource.kt @@ -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()) } - } - } diff --git a/src/main/java/com/smallhacker/disbrowser/util/json.kt b/src/main/java/com/smallhacker/disbrowser/util/json.kt index a2d6b47..0bf26e0 100644 --- a/src/main/java/com/smallhacker/disbrowser/util/json.kt +++ b/src/main/java/com/smallhacker/disbrowser/util/json.kt @@ -14,9 +14,10 @@ interface JsonFile { fun save(value: T) } -inline fun jsonFile(path: Path): JsonFile { +inline fun jsonFile(path: Path, prettyPrint: Boolean = false): JsonFile { + val writer = if (prettyPrint) jsonMapper.writerWithDefaultPrettyPrinter() else jsonMapper.writer() return object : JsonFile { override fun load() = jsonMapper.readValue(path.toFile()) - override fun save(value: T) = jsonMapper.writeValue(path.toFile(), value) + override fun save(value: T) = writer.writeValue(path.toFile(), value) } } \ No newline at end of file diff --git a/src/main/ts/main.ts b/src/main/ts/main.ts index ad3d977..db18ef0 100644 --- a/src/main/ts/main.ts +++ b/src/main/ts/main.ts @@ -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 = (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; }); } \ No newline at end of file diff --git a/src/main/ts/tsconfig.json b/src/main/ts/tsconfig.json index 24f70f2..c942057 100644 --- a/src/main/ts/tsconfig.json +++ b/src/main/ts/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "amd", + "target": "es2016", "strictNullChecks": true, "noImplicitAny": true, "noImplicitReturns": true,