import { Segment, SegmentType } from "../../../core/data_formats/asm/instructions"; import { Kind, OP_ADD, OP_ADD_MSG, OP_ADDI, OP_AND, OP_ANDI, OP_ARG_PUSHA, OP_ARG_PUSHB, OP_ARG_PUSHL, OP_ARG_PUSHR, OP_ARG_PUSHS, OP_ARG_PUSHW, OP_BB_MAP_DESIGNATE, OP_CALL, OP_CLEAR, OP_DIV, OP_DIVI, OP_EXIT, OP_FADD, OP_FADDI, OP_FDIV, OP_FDIVI, OP_FLET, OP_FLETI, OP_FMUL, OP_FMULI, OP_FSUB, OP_FSUBI, OP_GET_RANDOM, OP_GETTIME, OP_JMP, OP_JMP_E, OP_JMP_G, OP_JMP_GE, OP_JMP_L, OP_JMP_LE, OP_JMP_NE, OP_JMP_OFF, OP_JMP_ON, OP_JMPI_E, OP_JMPI_G, OP_JMPI_GE, OP_JMPI_L, OP_JMPI_LE, OP_JMPI_NE, OP_LET, OP_LETA, OP_LETB, OP_LETI, OP_LETW, OP_LIST, OP_MAP_DESIGNATE, OP_MAP_DESIGNATE_EX, OP_MOD, OP_MODI, OP_MUL, OP_MULI, OP_NOP, OP_OR, OP_ORI, OP_P_DEAD_V3, OP_RET, OP_REV, OP_SET, OP_SET_EPISODE, OP_SET_FLOOR_HANDLER, OP_SHIFT_LEFT, OP_SHIFT_RIGHT, OP_STACK_POP, OP_STACK_POPM, OP_STACK_PUSH, OP_STACK_PUSHM, OP_SUB, OP_SUBI, OP_SYNC, OP_THREAD, OP_THREAD_STG, OP_UJMP_G, OP_UJMP_GE, OP_UJMP_L, OP_UJMP_LE, OP_UJMPI_G, OP_UJMPI_GE, OP_UJMPI_L, OP_UJMPI_LE, OP_WINDOW_MSG, OP_WINEND, OP_XOR, OP_XORI, } from "../../../core/data_formats/asm/opcodes"; import { andreduce, andsecond, BinaryNumericOperation, ComparisonOperation, numeric_ops, rest, } from "./utils"; import { DefaultVirtualMachineIO, VirtualMachineIO } from "./io"; import { Episode } from "../../../core/data_formats/parsing/quest/Episode"; import { Endianness } from "../../../core/data_formats/Endianness"; import { Random } from "./Random"; import { Memory } from "./Memory"; import { InstructionPointer } from "./InstructionPointer"; import { StackFrame, StepMode, Thread } from "./Thread"; import { LogManager } from "../../../core/Logger"; export const REGISTER_COUNT = 256; const REGISTER_SIZE = 4; const VARIABLE_STACK_LENGTH = 16; // TODO: verify this value const STRING_ARG_STORE_ADDRESS = 0x00a92700; const STRING_ARG_STORE_SIZE = 1024; // TODO: verify this value const ENTRY_SEGMENT = 0; const LIST_ITEM_DELIMITER = "\n"; const logger = LogManager.get("quest_editor/scripting/vm/VirtualMachine"); export enum ExecutionResult { /** * There are no live threads, nothing to do. */ Suspended, /** * Execution is paused due to hitting a breakpoint or because the VM is executing in * stepping mode. */ Paused, /** * All threads have yielded. */ WaitingVsync, /** * Waiting for any keypress. No method call required. */ WaitingInput, /** * Waiting for a value to be selected in a list. * Call `list_select` to set selection. */ WaitingSelection, /** * Execution has halted because the VM encountered an `exit` instruction, a fatal error was * raised or the VM was halted from outside. */ Halted, } function encode_episode_number(ep: Episode): number { switch (ep) { case Episode.I: return 0; case Episode.II: return 1; case Episode.IV: return 2; } } /** * This class emulates the PSO script engine. It's in charge of memory, threading and executing * instructions. */ export class VirtualMachine { // Quest details. private episode: Episode = Episode.I; private _object_code: readonly Segment[] = []; private readonly label_to_seg_idx: Map = new Map(); // VM state. private readonly registers = new Memory(REGISTER_COUNT * REGISTER_SIZE, Endianness.Little); private string_arg_store = ""; private threads: Thread[] = []; private thread_idx = 0; private window_msg_open = false; private set_episode_called = false; private list_open = false; private selection_reg = 0; private _halted = true; // Debugging. private readonly breakpoints: InstructionPointer[] = []; private paused = false; /** * Set of unsupported opcodes that have already been logged. Each unsupported opcode will only * be logged once to avoid flooding the log with duplicate log messages. */ private readonly unsupported_opcodes_logged: Set = new Set(); get object_code(): readonly Segment[] { return this._object_code; } get halted(): boolean { return this._halted; } set step_mode(step_mode: StepMode) { if (step_mode != undefined) { const thread = this.current_thread(); if (thread) { thread.step_mode = step_mode; } } } constructor( private io: VirtualMachineIO = new DefaultVirtualMachineIO(), private random: Random = new Random(), ) {} /** * Halts and resets the VM, then loads new object code. */ load_object_code(object_code: readonly Segment[], episode: Episode): void { this.halt(); logger.debug("Starting."); this._object_code = object_code; this.episode = episode; this.label_to_seg_idx.clear(); let i = 0; for (const segment of this._object_code) { for (const label of segment.labels) { this.label_to_seg_idx.set(label, i); } i++; } this._halted = false; } /** * Schedules concurrent execution of the code at the given label. * * @param label - instruction label to start thread execution at. * @param area_id - if an area_id is passed in, the thread is floor-local. */ start_thread(label: number, area_id?: number): void { const seg_idx = this.get_segment_index_by_label(label); const segment = this._object_code[seg_idx]; if (segment.type !== SegmentType.Instructions) { throw new Error( `Label ${label} points to a ${SegmentType[segment.type]} segment, expecting ${ SegmentType[SegmentType.Instructions] }.`, ); } this.threads.push( new Thread(this.io, new InstructionPointer(seg_idx!, 0, this.object_code), area_id), ); } /** * Executes instructions while possible. */ execute(): ExecutionResult { if (this._halted) { return ExecutionResult.Halted; } let inst_ptr: InstructionPointer | undefined; try { // Limit amount of instructions executed to prevent infinite loops. let execution_counter = 0; while (execution_counter++ < 10_000) { // Check whether VM is waiting for vsync or is suspended. if (this.threads.length >= 1 && this.thread_idx >= this.threads.length) { return ExecutionResult.WaitingVsync; } const thread = this.current_thread(); if (!thread) { return ExecutionResult.Suspended; } // Get current instruction. const frame = thread.current_stack_frame()!; inst_ptr = frame.instruction_pointer; const inst = inst_ptr.instruction; // Check whether the VM needs to pause only if it's not already paused. In that case // it's resuming. if (!this.paused) { switch (thread.step_mode) { case StepMode.BreakPoint: if (this.breakpoints.findIndex(bp => bp.equals(inst_ptr!)) !== -1) { this.paused = true; return ExecutionResult.Paused; } break; case StepMode.Over: if ( thread.step_frame && frame.idx <= thread.step_frame.idx && inst.asm?.mnemonic ) { this.paused = true; return ExecutionResult.Paused; } break; case StepMode.In: if (inst.asm?.mnemonic) { this.paused = true; return ExecutionResult.Paused; } break; case StepMode.Out: if ( thread.step_frame && frame.idx < thread.step_frame.idx && inst.asm?.mnemonic ) { this.paused = true; return ExecutionResult.Paused; } break; } } // Not paused, the next instruction can be executed. this.paused = false; const result = this.execute_instruction(thread, inst_ptr); // Only return WaitingVsync when all threads have yielded. if (result != undefined && result !== ExecutionResult.WaitingVsync) { return result; } } throw new Error( "Maximum execution count reached. The code probably contains an infinite loop.", ); } catch (thrown) { let err = thrown; if (!(err instanceof Error)) { err = new Error(String(err)); } try { this.io.error(err, inst_ptr); } finally { this.halt(); } return ExecutionResult.Halted; } } /** * Signal to the VM that a vsync has happened. */ vsync(): void { if (this.thread_idx >= this.threads.length) { this.thread_idx = 0; } } /** * Halts execution of all threads. */ halt(): void { if (!this._halted) { logger.debug("Halting."); this.registers.zero(); this.string_arg_store = ""; this.threads = []; this.thread_idx = 0; this.window_msg_open = false; this.set_episode_called = false; this.list_open = false; this.selection_reg = 0; this._halted = true; this.paused = false; this.breakpoints.splice(0, Infinity); this.unsupported_opcodes_logged.clear(); } } get_current_stack_frame(): StackFrame | undefined { return this.current_thread()?.current_stack_frame(); } get_instruction_pointer(): InstructionPointer | undefined { return this.get_current_stack_frame()?.instruction_pointer; } get_segment_index_by_label(label: number): number { if (!this.label_to_seg_idx.has(label)) { throw new Error(`Invalid argument: No such label ${label}.`); } return this.label_to_seg_idx.get(label)!; } /** * When the list opcode is used, call this method to select a value in the list. */ list_select(idx: number): void { if (!this.list_open) { throw new Error("list_select may not be called if there is no list open"); } this.set_register_unsigned(this.selection_reg, idx); } set_breakpoint(breakpoint: InstructionPointer): void { if (this.breakpoints.findIndex(bp => bp.equals(breakpoint)) === -1) { this.breakpoints.push(breakpoint); } } remove_breakpoint(breakpoint: InstructionPointer): void { const index = this.breakpoints.findIndex(bp => bp.equals(breakpoint)); if (index !== -1) { this.breakpoints.splice(index, 1); } } private current_thread(): Thread | undefined { return this.threads[this.thread_idx]; } private terminate_thread(thread_idx: number): void { this.threads.splice(thread_idx, 1); if (this.thread_idx >= thread_idx && this.thread_idx > 0) { this.thread_idx--; } } /** * Advance to the next instruction. */ private advance(thread: Thread): void { const frame = thread.current_stack_frame(); if (!frame) return; // Thread already terminated. const next = frame.instruction_pointer.next(); if (next) { thread.set_current_instruction_pointer(next); } else { // Reached EOF. // Game will crash if call stack is not empty. if (thread.call_stack.length > 0) { throw new Error("Reached EOF but call stack was not empty"); } thread.pop_call_stack(); this.terminate_thread(this.thread_idx); } } private execute_instruction( thread: Thread, inst_ptr: InstructionPointer, ): ExecutionResult | undefined { const inst = inst_ptr.instruction; let result: ExecutionResult | undefined = undefined; let advance = true; const arg_vals = inst.args.map(arg => arg.value); const [arg0, arg1, arg2] = arg_vals; // previous instruction must've been `list`. // list may not exist after the instruction if (this.list_open) { this.list_open = false; } const stack_args = thread.fetch_args(inst_ptr); switch (inst.opcode.code) { case OP_NOP.code: break; case OP_RET.code: this.pop_call_stack(this.thread_idx); break; case OP_SYNC.code: result = ExecutionResult.WaitingVsync; this.advance(thread); this.thread_idx++; advance = false; break; case OP_EXIT.code: this.halt(); break; case OP_THREAD.code: this.start_thread(arg0); break; // integer lets case OP_LET.code: this.set_register_signed(arg0, this.get_register_signed(arg1)); break; case OP_LETI.code: this.set_register_signed(arg0, arg1); break; case OP_LETB.code: this.set_register_byte(arg0, arg1); break; case OP_LETW.code: this.set_register_word(arg0, arg1); break; case OP_LETA.code: this.set_register_unsigned(arg0, this.get_register_address(arg0)); break; // float lets case OP_FLET.code: this.set_register_float(arg0, this.get_register_float(arg1)); break; case OP_FLETI.code: this.set_register_float(arg0, arg1); break; case OP_SET.code: this.set_register_signed(arg0, 1); break; case OP_CLEAR.code: this.set_register_signed(arg0, 0); break; case OP_REV.code: this.set_register_signed(arg0, this.get_register_signed(arg0) === 0 ? 1 : 0); break; case OP_CALL.code: this.push_call_stack(thread, arg0); advance = false; break; case OP_JMP.code: this.jump_to_label(thread, arg0); advance = false; break; case OP_ARG_PUSHR.code: // deref given register ref thread.push_arg(this.get_register_signed(arg0), Kind.DWord); break; case OP_ARG_PUSHL.code: thread.push_arg(inst.args[0].value, Kind.DWord); break; case OP_ARG_PUSHB.code: thread.push_arg(inst.args[0].value, Kind.Byte); break; case OP_ARG_PUSHW.code: thread.push_arg(inst.args[0].value, Kind.Word); break; case OP_ARG_PUSHA.code: // push address of register thread.push_arg(this.get_register_address(inst.args[0].value), Kind.DWord); break; case OP_ARG_PUSHS.code: if (typeof arg0 === "string") { // process tags const string_arg = this.parse_template_string(arg0); // store string and push its address this.string_arg_store = string_arg.slice(0, STRING_ARG_STORE_SIZE / 2); thread.push_arg(STRING_ARG_STORE_ADDRESS, Kind.String); } break; // integer arithmetic operations case OP_ADD.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.add); break; case OP_ADDI.code: this.do_integer_op_with_literal(arg0, arg1, numeric_ops.add); break; case OP_SUB.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.sub); break; case OP_SUBI.code: this.do_integer_op_with_literal(arg0, arg1, numeric_ops.sub); break; case OP_MUL.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.mul); break; case OP_MULI.code: this.do_integer_op_with_literal(arg0, arg1, numeric_ops.mul); break; case OP_DIV.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.idiv); break; case OP_DIVI.code: this.do_integer_op_with_literal(arg0, arg1, numeric_ops.idiv); break; case OP_MOD.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.mod); break; case OP_MODI.code: this.do_integer_op_with_literal(arg0, arg1, numeric_ops.mod); break; // float arithmetic operations case OP_FADD.code: this.do_float_op_with_register(arg0, arg1, numeric_ops.add); break; case OP_FADDI.code: this.do_float_op_with_literal(arg0, Math.fround(arg1), numeric_ops.add); break; case OP_FSUB.code: this.do_float_op_with_register(arg0, arg1, numeric_ops.sub); break; case OP_FSUBI.code: this.do_float_op_with_literal(arg0, Math.fround(arg1), numeric_ops.sub); break; case OP_FMUL.code: this.do_float_op_with_register(arg0, arg1, numeric_ops.mul); break; case OP_FMULI.code: this.do_float_op_with_literal(arg0, Math.fround(arg1), numeric_ops.mul); break; case OP_FDIV.code: this.do_float_op_with_register(arg0, arg1, numeric_ops.div); break; case OP_FDIVI.code: this.do_float_op_with_literal(arg0, Math.fround(arg1), numeric_ops.div); break; // bit operations case OP_AND.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.and); break; case OP_ANDI.code: this.do_integer_op_with_literal(arg0, arg1, numeric_ops.and); break; case OP_OR.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.or); break; case OP_ORI.code: this.do_integer_op_with_literal(arg0, arg1, numeric_ops.or); break; case OP_XOR.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.xor); break; case OP_XORI.code: this.do_integer_op_with_literal(arg0, arg1, numeric_ops.xor); break; // shift operations case OP_SHIFT_LEFT.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.shl); break; case OP_SHIFT_RIGHT.code: this.do_integer_op_with_register(arg0, arg1, numeric_ops.shr); break; // conditional jumps case OP_JMP_ON.code: // all nonzero? this.conditional_jump( thread, arg0, (_, b) => b !== 0, 1, ...rest(arg_vals).map(reg => this.get_register_signed(reg)), ); advance = false; break; case OP_JMP_OFF.code: // all eq 0? this.conditional_jump( thread, arg0, (a, b) => a === b, 0, ...rest(arg_vals).map(reg => this.get_register_signed(reg)), ); advance = false; break; case OP_JMP_E.code: this.signed_cond_jump_with_register(thread, arg2, (a, b) => a === b, arg0, arg1); advance = false; break; case OP_JMPI_E.code: this.signed_cond_jump_with_literal(thread, arg2, (a, b) => a === b, arg0, arg1); advance = false; break; case OP_JMP_NE.code: this.signed_cond_jump_with_register(thread, arg2, (a, b) => a !== b, arg0, arg1); advance = false; break; case OP_JMPI_NE.code: this.signed_cond_jump_with_literal(thread, arg2, (a, b) => a !== b, arg0, arg1); advance = false; break; case OP_UJMP_G.code: this.unsigned_cond_jump_with_register(thread, arg2, (a, b) => a > b, arg0, arg1); advance = false; break; case OP_UJMPI_G.code: this.unsigned_cond_jump_with_literal(thread, arg2, (a, b) => a > b, arg0, arg1); advance = false; break; case OP_JMP_G.code: this.signed_cond_jump_with_register(thread, arg2, (a, b) => a > b, arg0, arg1); advance = false; break; case OP_JMPI_G.code: this.signed_cond_jump_with_literal(thread, arg2, (a, b) => a > b, arg0, arg1); advance = false; break; case OP_UJMP_L.code: this.unsigned_cond_jump_with_register(thread, arg2, (a, b) => a < b, arg0, arg1); advance = false; break; case OP_UJMPI_L.code: this.unsigned_cond_jump_with_literal(thread, arg2, (a, b) => a < b, arg0, arg1); advance = false; break; case OP_JMP_L.code: this.signed_cond_jump_with_register(thread, arg2, (a, b) => a < b, arg0, arg1); advance = false; break; case OP_JMPI_L.code: this.signed_cond_jump_with_literal(thread, arg2, (a, b) => a < b, arg0, arg1); advance = false; break; case OP_UJMP_GE.code: this.unsigned_cond_jump_with_register(thread, arg2, (a, b) => a >= b, arg0, arg1); advance = false; break; case OP_UJMPI_GE.code: this.unsigned_cond_jump_with_literal(thread, arg2, (a, b) => a >= b, arg0, arg1); advance = false; break; case OP_JMP_GE.code: this.signed_cond_jump_with_register(thread, arg2, (a, b) => a >= b, arg0, arg1); advance = false; break; case OP_JMPI_GE.code: this.signed_cond_jump_with_literal(thread, arg2, (a, b) => a >= b, arg0, arg1); advance = false; break; case OP_UJMP_LE.code: this.unsigned_cond_jump_with_register(thread, arg2, (a, b) => a <= b, arg0, arg1); advance = false; break; case OP_UJMPI_LE.code: this.unsigned_cond_jump_with_literal(thread, arg2, (a, b) => a <= b, arg0, arg1); advance = false; break; case OP_JMP_LE.code: this.signed_cond_jump_with_register(thread, arg2, (a, b) => a <= b, arg0, arg1); advance = false; break; case OP_JMPI_LE.code: this.signed_cond_jump_with_literal(thread, arg2, (a, b) => a <= b, arg0, arg1); advance = false; break; // variable stack operations case OP_STACK_PUSH.code: this.push_variable_stack(thread, arg0, 1); break; case OP_STACK_POP.code: this.pop_variable_stack(thread, arg0, 1); break; case OP_STACK_PUSHM.code: this.push_variable_stack(thread, arg0, arg1); break; case OP_STACK_POPM.code: this.pop_variable_stack(thread, arg0, arg1); break; case OP_LIST.code: if (!this.window_msg_open) { const list_items = this.deref_string(stack_args[1]).split(LIST_ITEM_DELIMITER); result = ExecutionResult.WaitingSelection; this.list_open = true; this.selection_reg = stack_args[0]; this.io.list(list_items); } break; case OP_WINDOW_MSG.code: if (!this.window_msg_open) { const str = this.deref_string(stack_args[0]); result = ExecutionResult.WaitingInput; this.window_msg_open = true; this.io.window_msg(str); } break; case OP_ADD_MSG.code: if (this.window_msg_open) { const str = this.deref_string(stack_args[0]); result = ExecutionResult.WaitingInput; this.io.add_msg(str); } break; case OP_GETTIME.code: this.set_register_unsigned(arg0, Math.floor(Date.now() / 1000)); break; case OP_WINEND.code: if (this.window_msg_open) { this.window_msg_open = false; this.io.winend(); } break; case OP_P_DEAD_V3.code: this.set_register_signed(stack_args[0], this.io.p_dead_v3(stack_args[1]) ? 1 : 0); break; case OP_SET_FLOOR_HANDLER.code: this.io.set_floor_handler(stack_args[0], stack_args[1]); break; case OP_THREAD_STG.code: this.start_thread(arg0); break; case OP_MAP_DESIGNATE.code: this.io.map_designate( this.get_register_signed(arg0), this.get_register_signed(arg0 + 2), ); break; case OP_MAP_DESIGNATE_EX.code: this.io.map_designate( this.get_register_signed(arg0), this.get_register_signed(arg0 + 3), ); break; case OP_GET_RANDOM.code: { const low = this.get_register_signed(arg0); const hi = this.get_register_signed(arg0 + 1); const r = this.random.next(); let result = Math.floor(Math.fround(r / 32768.0) * hi); // intentional. this is how the game does it. if (low >= result) { result = low; } this.set_register_signed(arg1, result); } break; case OP_SET_EPISODE.code: if (this.set_episode_called) { this.io.warning( "Calling set_episode more than once is not supported.", inst_ptr, ); break; } this.set_episode_called = true; if (!this._object_code[inst_ptr.seg_idx].labels.includes(ENTRY_SEGMENT)) { this.io.warning( `Calling set_episode outside of segment ${ENTRY_SEGMENT} is not supported.`, inst_ptr, ); break; } if (encode_episode_number(this.episode) !== arg0) { this.io.warning( "Calling set_episode with an argument that does not " + "match the quest's designated episode is not supported.", inst_ptr, ); break; } break; case OP_BB_MAP_DESIGNATE.code: this.io.map_designate(arg0, arg2); break; default: if (!this.unsupported_opcodes_logged.has(inst.opcode.code)) { this.unsupported_opcodes_logged.add(inst.opcode.code); this.io.warning(`Unsupported instruction.`, inst_ptr); } break; } if (advance) { this.advance(thread); } return result; } public get_register_signed(reg: number): number { return this.registers.i32_at(REGISTER_SIZE * reg); } private set_register_signed(reg: number, value: number): void { this.registers.write_i32_at(REGISTER_SIZE * reg, value); } public get_register_unsigned(reg: number): number { return this.registers.u32_at(REGISTER_SIZE * reg); } private set_register_unsigned(reg: number, value: number): void { this.registers.write_u32_at(REGISTER_SIZE * reg, value); } public get_register_word(reg: number): number { return this.registers.u16_at(REGISTER_SIZE * reg); } private set_register_word(reg: number, value: number): void { this.registers.write_u16_at(REGISTER_SIZE * reg, value); } public get_register_byte(reg: number): number { return this.registers.u8_at(REGISTER_SIZE * reg); } public set_register_byte(reg: number, value: number): void { this.registers.write_u8_at(REGISTER_SIZE * reg, value); } public get_register_float(reg: number): number { return this.registers.f32_at(REGISTER_SIZE * reg); } private set_register_float(reg: number, value: number): void { this.registers.write_f32_at(REGISTER_SIZE * reg, value); } private do_integer_op_with_register( reg1: number, reg2: number, op: BinaryNumericOperation, ): void { this.do_integer_op_with_literal(reg1, this.get_register_signed(reg2), op); } private do_integer_op_with_literal( reg: number, literal: number, op: BinaryNumericOperation, ): void { if ((op === numeric_ops.div || op === numeric_ops.idiv) && literal === 0) { throw new Error("Division by zero"); } this.set_register_signed(reg, op(this.get_register_signed(reg), literal)); } private do_float_op_with_register( reg1: number, reg2: number, op: BinaryNumericOperation, ): void { this.do_float_op_with_literal(reg1, this.get_register_float(reg2), op); } private do_float_op_with_literal( reg: number, literal: number, op: BinaryNumericOperation, ): void { if ((op === numeric_ops.div || op === numeric_ops.idiv) && literal === 0) { throw new Error("Division by zero"); } this.set_register_float(reg, op(this.get_register_float(reg), literal)); } private push_call_stack(thread: Thread, label: number): void { const seg_idx = this.get_segment_index_by_label(label); const segment = this._object_code[seg_idx]; if (segment.type !== SegmentType.Instructions) { throw new Error( `Label ${label} points to a ${SegmentType[segment.type]} segment, expecting ${ SegmentType[SegmentType.Instructions] }.`, ); } else { thread.push_frame(new InstructionPointer(seg_idx, 0, this.object_code)); } } private pop_call_stack(idx: number): void { this.threads[idx].pop_call_stack(); if (this.threads[idx].call_stack.length === 0) { // 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.terminate_thread(idx); } } private jump_to_label(thread: Thread, label: number): void { thread.set_current_instruction_pointer( new InstructionPointer(this.get_segment_index_by_label(label), 0, this.object_code), ); } private signed_cond_jump_with_register( exec: Thread, label: number, condition: ComparisonOperation, reg1: number, reg2: number, ): void { this.conditional_jump( exec, label, condition, this.get_register_signed(reg1), this.get_register_signed(reg2), ); } private signed_cond_jump_with_literal( exec: Thread, label: number, condition: ComparisonOperation, reg: number, literal: number, ): void { this.conditional_jump(exec, label, condition, this.get_register_signed(reg), literal); } private unsigned_cond_jump_with_register( exec: Thread, label: number, condition: ComparisonOperation, reg1: number, reg2: number, ): void { this.conditional_jump( exec, label, condition, this.get_register_unsigned(reg1), this.get_register_unsigned(reg2), ); } private unsigned_cond_jump_with_literal( exec: Thread, label: number, condition: ComparisonOperation, reg: number, literal: number, ): void { this.conditional_jump(exec, label, condition, this.get_register_unsigned(reg), literal); } private conditional_jump( thread: Thread, label: number, condition: ComparisonOperation, ...vals: number[] ): void { const chain_cmp = andsecond.bind< null, ComparisonOperation, Parameters, any >(null, condition); if (andreduce(chain_cmp, vals) !== undefined) { this.jump_to_label(thread, label); } else { this.advance(thread); } } private push_variable_stack(exec: Thread, base_reg: number, num_push: number): void { const end = base_reg + num_push; if (end > REGISTER_COUNT) { throw new Error("Variable stack: Invalid register"); } if (exec.variable_stack.length + num_push > VARIABLE_STACK_LENGTH) { throw new Error("Variable stack: Stack overflow"); } for (let r = base_reg; r < end; r++) { exec.variable_stack.push(this.get_register_unsigned(r)); } } private pop_variable_stack(exec: Thread, base_reg: number, num_pop: number): void { const end = base_reg + num_pop; if (end > REGISTER_COUNT) { throw new Error("Variable stack: Invalid register"); } if (exec.variable_stack.length < num_pop) { throw new Error("Variable stack: Stack underflow"); } for (let r = end - 1; r >= base_reg; r--) { this.set_register_unsigned(r, exec.variable_stack.pop()!); } } private get_register_address(reg: number): number { return reg * REGISTER_SIZE; } private deref_string(address: number): string { if (address === STRING_ARG_STORE_ADDRESS) { return this.string_arg_store; } if (address > 0 && address < REGISTER_COUNT * REGISTER_SIZE) { return this.registers.string_utf16_at(address, REGISTER_COUNT * REGISTER_SIZE, true); } throw new Error(`Failed to dereference string: Invalid address ${address}`); } private parse_template_string(template: string): string { const exact_tags: Record string)> = { // TODO: get real values for these "hero name": "PLACEHOLDER", "hero job": "PLACEHOLDER", "name hero": "PLACEHOLDER", "name job": "PLACEHOLDER", // intentionally hardcoded time: "01:12", // used in cmode "award item": "PLACEHOLDER", "challenge title": "PLACEHOLDER", // set by opcode get_pl_name pl_name: "PLACEHOLDER", pl_job: "PLACEHOLDER", last_word: "PLACEHOLDER", last_chat: "PLACEHOLDER", team_name: "PLACEHOLDER", // does not appear to be used in any sega quests meseta_slot_prize: "PLACEHOLDER", }; const pattern_tags: [RegExp, (arg: string) => string][] = [ [ /^color ([0-9]+)$/, arg => { // TODO: decide how to handle this return ``; }, ], [ /^r([0-9]{1,3})$/, arg => { const num = parseInt(arg); if (isNaN(num)) { return ""; } return this.get_register_unsigned(num).toString(); }, ], [ /^f([0-9]{1,3})$/, arg => { const num = parseInt(arg); if (isNaN(num)) { return ""; } return this.get_register_float(num).toFixed(6); }, ], ]; const tag_start_char = "<"; const tag_end_char = ">"; let tag_open = false; let tag_start_idx = -1; let i = 0; let len = template.length; // iterate through template while (i < len) { const char = template[i]; // end of tag if (tag_open && char === tag_end_char) { tag_open = false; // extract key from tag const tag_end_idx = i; const key = template.slice(tag_start_idx + 1, tag_end_idx); // get value let val: string | (() => string) | undefined = undefined; // check if matches pattern for (const [pattern, handler] of pattern_tags) { const match = pattern.exec(key); if (match && match[1] !== undefined) { val = handler(match[1]); break; } } // check if matches tag if (val === undefined) { val = exact_tags[key]; } // not a valid tag, replace with empty string if (val === undefined) { val = ""; } // run function and memoize result else if (typeof val === "function") { const memo = val(); exact_tags[key] = memo; val = memo; } // replace tag with value in template template = template.slice(0, tag_start_idx) + val + template.slice(tag_end_idx + 1); // adjust position const offset = val.length - (key.length + 2); i += offset; len += offset; } // mark start of tag else if (char === tag_start_char) { tag_open = true; tag_start_idx = i; } i++; } // remove open tag if it was not closed until the end if (tag_open) { template = template.slice(0, tag_start_idx); } return template; } }