From 8c102976a81202aaf4708fef1553026bd7486d9e Mon Sep 17 00:00:00 2001 From: jtuu Date: Thu, 21 Nov 2019 23:04:16 +0200 Subject: [PATCH] Added a VM registers viewer. --- src/core/util.ts | 14 ++ src/quest_editor/QuestRunner.ts | 2 +- src/quest_editor/gui/QuestEditorView.ts | 10 +- src/quest_editor/gui/RegistersView.css | 31 +++++ src/quest_editor/gui/RegistersView.ts | 174 ++++++++++++++++++++++++ src/quest_editor/scripting/vm/index.ts | 2 +- 6 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 src/quest_editor/gui/RegistersView.css create mode 100644 src/quest_editor/gui/RegistersView.ts diff --git a/src/core/util.ts b/src/core/util.ts index 74b168c8..792e3e76 100644 --- a/src/core/util.ts +++ b/src/core/util.ts @@ -59,3 +59,17 @@ export function assert(condition: any, msg?: string): asserts condition { throw new Error(full_msg); } } + +export function number_to_hex_string( + num: number, + min_len: number = 8, + prefix: string = "0x", +): string { + let result = num.toString(16); + + if (result.length < min_len) { + result = "0".repeat(min_len - result.length) + result; + } + + return prefix + result; +} diff --git a/src/quest_editor/QuestRunner.ts b/src/quest_editor/QuestRunner.ts index 0cc63fda..78a8c85c 100644 --- a/src/quest_editor/QuestRunner.ts +++ b/src/quest_editor/QuestRunner.ts @@ -28,7 +28,7 @@ function srcloc_to_string(srcloc: AsmToken): string { } export class QuestRunner { - private readonly vm: VirtualMachine; + public readonly vm: VirtualMachine; private quest?: QuestModel; private animation_frame?: number; /** diff --git a/src/quest_editor/gui/QuestEditorView.ts b/src/quest_editor/gui/QuestEditorView.ts index 2b36964b..5a9d3f1e 100644 --- a/src/quest_editor/gui/QuestEditorView.ts +++ b/src/quest_editor/gui/QuestEditorView.ts @@ -17,6 +17,7 @@ import { ObjectListView } from "./ObjectListView"; import { EventsView } from "./EventsView"; import Logger = require("js-logger"); import { QuestMessageLogView } from "./QuestMessageLogView"; +import { RegistersView } from "./RegistersView"; const logger = Logger.get("quest_editor/gui/QuestEditorView"); @@ -30,6 +31,7 @@ const VIEW_TO_NAME = new Map ResizableWidget, string>([ [NpcListView, "npc_list_view"], [ObjectListView, "object_list_view"], [QuestMessageLogView, "message_log_view"], + [RegistersView, "registers_view"], ]); if (gui_store.feature_active("events")) { @@ -106,7 +108,13 @@ const DEFAULT_LAYOUT_CONTENT: ItemConfigType[] = [ title: "Log", type: "component", componentName: VIEW_TO_NAME.get(QuestMessageLogView), - isClosable: true, + isClosable: false, + }, + { + title: "Registers", + type: "component", + componentName: VIEW_TO_NAME.get(RegistersView), + isClosable: false, }, ], }, diff --git a/src/quest_editor/gui/RegistersView.css b/src/quest_editor/gui/RegistersView.css new file mode 100644 index 00000000..9f270e44 --- /dev/null +++ b/src/quest_editor/gui/RegistersView.css @@ -0,0 +1,31 @@ +.quest_editor_RegistersView { + font-family: monospace; +} + +.quest_editor_RegistersView_container { + overflow: auto; + height: 97%; +} + +.quest_editor_RegistersView_list { + padding: 0.5em; + column-width: 9.8em; +} + +.quest_editor_RegistersView_register { + display: flex; + align-items: baseline; +} + +.quest_editor_RegistersView_register > .core_Label { + width: 2.8em; +} + +.quest_editor_RegistersView_value { + width: 7em; +} + +.quest_editor_RegistersView_value > .core_TextInput_inner { + text-align: right; + font-family: monospace; +} diff --git a/src/quest_editor/gui/RegistersView.ts b/src/quest_editor/gui/RegistersView.ts new file mode 100644 index 00000000..a328a6f1 --- /dev/null +++ b/src/quest_editor/gui/RegistersView.ts @@ -0,0 +1,174 @@ +import { ResizableWidget } from "../../core/gui/ResizableWidget"; +import { quest_editor_store } from "../stores/QuestEditorStore"; +import { el } from "../../core/gui/dom"; +import { REGISTER_COUNT } from "../scripting/vm"; +import { TextInput } from "../../core/gui/TextInput"; +import { DropDown } from "../../core/gui/DropDown"; +import { ToolBar } from "../../core/gui/ToolBar"; +import { CheckBox } from "../../core/gui/CheckBox"; +import { number_to_hex_string } from "../../core/util"; +import "./RegistersView.css"; + +enum RegisterDisplayType { + Signed, + Unsigned, + Word, + Byte, + Float, +} + +type RegisterGetterFunction = (register: number) => number; + +export class RegistersView extends ResizableWidget { + private readonly type_dropdown = this.disposable( + new DropDown( + "Display type", + [ + RegisterDisplayType.Signed, + RegisterDisplayType.Unsigned, + RegisterDisplayType.Word, + RegisterDisplayType.Byte, + RegisterDisplayType.Float, + ], + type => RegisterDisplayType[type], + { + tooltip: "Select which data type register values should be displayed as.", + }, + ), + ); + private register_getter: RegisterGetterFunction = this.get_register_getter( + RegisterDisplayType.Unsigned, + ); + + private readonly hex_checkbox = this.disposable( + new CheckBox(false, { + label: "Hex", + tooltip: "Display register values in hexadecimal.", + }), + ); + + private readonly settings_bar = this.disposable( + new ToolBar({ + children: [this.type_dropdown, this.hex_checkbox], + }), + ); + + private readonly register_els: TextInput[]; + private readonly list_element = el.div({ class: "quest_editor_RegistersView_list" }); + private readonly container_element = el.div( + { class: "quest_editor_RegistersView_container" }, + this.list_element, + ); + public readonly element = el.div( + { class: "quest_editor_RegistersView" }, + this.settings_bar.element, + this.container_element, + ); + + constructor() { + super(); + + // create register elements + const register_els: TextInput[] = Array(REGISTER_COUNT); + for (let i = 0; i < REGISTER_COUNT; i++) { + const value_el = this.disposable( + new TextInput("", { + class: "quest_editor_RegistersView_value", + label: "r" + i, + readonly: true, + }), + ); + + const wrapper_el = el.div( + { class: "quest_editor_RegistersView_register" }, + value_el.label!.element, + value_el.element, + ); + + register_els[i] = value_el; + + this.list_element.appendChild(wrapper_el); + } + this.register_els = register_els; + + // predicate that indicates whether to display + // placeholder text or the actual register values + const should_use_placeholders = (): boolean => + !quest_editor_store.quest_runner.paused.val || + !quest_editor_store.quest_runner.running.val; + + // set initial values + this.update(should_use_placeholders(), this.hex_checkbox.checked.val); + + this.disposables( + // check if values need to be updated + // when QuestRunner execution state changes + quest_editor_store.quest_runner.running.observe(() => + this.update(should_use_placeholders(), this.hex_checkbox.checked.val), + ), + quest_editor_store.quest_runner.paused.observe(() => + this.update(should_use_placeholders(), this.hex_checkbox.checked.val), + ), + + this.type_dropdown.chosen.observe(change => { + this.register_getter = this.get_register_getter(change.value); + this.update(should_use_placeholders(), this.hex_checkbox.checked.val); + }), + + this.hex_checkbox.checked.observe(change => + this.update(should_use_placeholders(), change.value), + ), + ); + + this.finalize_construction(RegistersView.prototype); + } + + private get_register_getter(typ: RegisterDisplayType): RegisterGetterFunction { + let getter: RegisterGetterFunction; + + switch (typ) { + case RegisterDisplayType.Signed: + getter = quest_editor_store.quest_runner.vm.get_register_signed; + break; + case RegisterDisplayType.Unsigned: + getter = quest_editor_store.quest_runner.vm.get_register_unsigned; + break; + case RegisterDisplayType.Word: + getter = quest_editor_store.quest_runner.vm.get_register_word; + break; + case RegisterDisplayType.Byte: + getter = quest_editor_store.quest_runner.vm.get_register_byte; + break; + case RegisterDisplayType.Float: + getter = quest_editor_store.quest_runner.vm.get_register_float; + break; + } + + return getter.bind(quest_editor_store.quest_runner.vm); + } + + private update(use_placeholders: boolean, use_hex: boolean): void { + if (use_placeholders) { + const placeholder_text = "??"; + for (let i = 0; i < REGISTER_COUNT; i++) { + const reg_el = this.register_els[i]; + + reg_el.value.set_val(placeholder_text, { silent: true }); + } + } else if (use_hex) { + for (let i = 0; i < REGISTER_COUNT; i++) { + const reg_el = this.register_els[i]; + const reg_val = quest_editor_store.quest_runner.vm.get_register_unsigned(i); + + reg_el.value.set_val(number_to_hex_string(reg_val), { silent: true }); + } + } else { + for (let i = 0; i < REGISTER_COUNT; i++) { + const reg_el = this.register_els[i]; + const reg_val = this.register_getter(i); + + reg_el.value.set_val(reg_val.toString(), { silent: true }); + } + } + } +} diff --git a/src/quest_editor/scripting/vm/index.ts b/src/quest_editor/scripting/vm/index.ts index b4955516..9b58d399 100644 --- a/src/quest_editor/scripting/vm/index.ts +++ b/src/quest_editor/scripting/vm/index.ts @@ -102,7 +102,7 @@ import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; import { ArrayBufferCursor } from "../../../core/data_formats/cursor/ArrayBufferCursor"; import { Endianness } from "../../../core/data_formats/Endianness"; -const REGISTER_COUNT = 256; +export const REGISTER_COUNT = 256; const REGISTER_SIZE = 4; const VARIABLE_STACK_LENGTH = 16; // TODO: verify this value const ARG_STACK_SLOT_SIZE = 4;