[VM] Added a button to run the current quest in the VM.

Made the VM work with output from the assembler.
Added some comments to the VM and cleaned it up a little bit.
This commit is contained in:
jtuu 2019-09-14 19:50:03 +03:00
parent bffd8f51d9
commit 80a4aa784f
3 changed files with 80 additions and 17 deletions

View File

@ -69,6 +69,10 @@ export class QuestEditorToolBar extends ToolBar {
}
},
);
const run_button = new Button("Run in VM", {
icon_left: Icon.Play,
tooltip: "[Experimental] Run the current quest in a virtual machine."
});
super({
children: [
@ -78,6 +82,7 @@ export class QuestEditorToolBar extends ToolBar {
undo_button,
redo_button,
area_select,
run_button,
],
});
@ -109,6 +114,8 @@ export class QuestEditorToolBar extends ToolBar {
quest_editor_store.set_current_area(area),
),
run_button.click.observe(() => quest_editor_store.run_current_quest_in_vm()),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", () =>
open_file_button.click(),
),

View File

@ -13,6 +13,7 @@ import {
OP_SET,
OP_SYNC,
OP_THREAD,
OP_JMP,
} from "../opcodes";
import Logger from "js-logger";
@ -74,7 +75,7 @@ export class VirtualMachine {
);
}
this.thread.push(new Thread(new StackElement(seg_idx!, 0), true));
this.thread.push(new Thread(new ExecutionLocation(seg_idx!, 0), true));
}
/**
@ -126,17 +127,27 @@ export class VirtualMachine {
case OP_CALL:
this.push_call_stack(exec, inst.args[0].value);
break;
case OP_JMP:
this.jump_to_label(exec, inst.args[0].value);
break;
default:
throw new Error(`Unsupported instruction: ${inst.opcode.mnemonic}.`);
}
if (exec.stack.length) {
const top = exec.stack_top();
// advance instruction "pointer"
if (exec.call_stack.length) {
const top = exec.call_stack_top();
const segment = this.object_code[top.seg_idx] as InstructionSegment;
// move to next instruction
if (++top.inst_idx >= segment.instructions.length) {
top.seg_idx++;
top.inst_idx = 0;
// segment ended, move to next segment
if (++top.seg_idx >= this.object_code.length) {
// eof
this.thread.splice(this.thread_idx, 1);
} else {
top.inst_idx = 0;
}
}
}
@ -189,29 +200,44 @@ export class VirtualMachine {
}.`,
);
} else {
exec.stack.push(new StackElement(seg_idx, -1));
exec.call_stack.push(new ExecutionLocation(seg_idx, -1));
}
}
}
private pop_call_stack(idx: number, exec: Thread): void {
exec.stack.pop();
exec.call_stack.pop();
if (exec.stack.length >= 1) {
const top = exec.stack_top();
if (exec.call_stack.length >= 1) {
const top = exec.call_stack_top();
const segment = this.object_code[top.seg_idx];
if (!segment || segment.type !== SegmentType.Instructions) {
throw new Error(`Invalid segment index ${top.seg_idx}.`);
}
} else {
// popped off the last return address
// which means this is the end of the function this thread was started on
// which means this is the end of this thread
this.thread.splice(idx, 1);
}
}
private jump_to_label(exec: Thread, label: number) {
const top = exec.call_stack_top();
const seg_idx = this.label_to_seg_idx.get(label);
if (seg_idx == undefined) {
logger.warn(`Invalid jump label: ${label}.`);
} else {
top.seg_idx = seg_idx;
top.inst_idx = -1;
}
}
private get_next_instruction_from_thread(exec: Thread): Instruction {
if (exec.stack.length) {
const top = exec.stack_top();
if (exec.call_stack.length) {
const top = exec.call_stack_top();
const segment = this.object_code[top.seg_idx];
if (!segment || segment.type !== SegmentType.Instructions) {
@ -237,7 +263,7 @@ export class VirtualMachine {
}
}
class StackElement {
class ExecutionLocation {
constructor(public seg_idx: number, public inst_idx: number) {}
}
@ -245,18 +271,18 @@ class Thread {
/**
* Call stack. The top element describes the instruction about to be executed.
*/
public stack: StackElement[] = [];
public call_stack: ExecutionLocation[] = [];
/**
* Global or floor-local?
*/
public global: boolean;
stack_top(): StackElement {
return this.stack[this.stack.length - 1];
call_stack_top(): ExecutionLocation {
return this.call_stack[this.call_stack.length - 1];
}
constructor(next: StackElement, global: boolean) {
this.stack = [next];
constructor(next: ExecutionLocation, global: boolean) {
this.call_stack = [next];
this.global = global;
}
}

View File

@ -28,6 +28,7 @@ import { RemoveEntityAction } from "../actions/RemoveEntityAction";
import { Euler, Vector3 } from "three";
import { vec3_to_threejs } from "../../core/rendering/conversion";
import { RotateEntityAction } from "../actions/RotateEntityAction";
import { VirtualMachine, ExecutionResult } from "../scripting/vm";
import Logger = require("js-logger");
const logger = Logger.get("quest_editor/gui/QuestEditorStore");
@ -350,6 +351,35 @@ export class QuestEditorStore implements Disposable {
logger.warn(`Section ${entity.section_id.val} not found.`);
}
};
run_current_quest_in_vm = () => {
logger.setLevel(logger.TRACE);
const quest = this.current_quest.val;
if (!quest) {
throw new Error("No quest");
}
const vm = new VirtualMachine();
vm.load_object_code(quest.object_code);
vm.start_thread(0);
exec_loop:
while (true) {
const exec_result = vm.execute();
switch (exec_result) {
case ExecutionResult.Ok:
break;
case ExecutionResult.WaitingVsync:
vm.vsync();
break;
case ExecutionResult.Halted:
break exec_loop;
}
}
}
}
export const quest_editor_store = new QuestEditorStore();