diff --git a/src/core/data_formats/parsing/quest/Version.ts b/src/core/data_formats/parsing/quest/Version.ts new file mode 100644 index 00000000..52ddeb76 --- /dev/null +++ b/src/core/data_formats/parsing/quest/Version.ts @@ -0,0 +1,15 @@ +export enum Version { + /** + * Dreamcast + */ + DC, + /** + * GameCube + */ + GC, + PC, + /** + * BlueBurst + */ + BB, +} diff --git a/src/core/data_formats/parsing/quest/bin.test.ts b/src/core/data_formats/parsing/quest/bin.test.ts index b5af94b5..ec5752f0 100644 --- a/src/core/data_formats/parsing/quest/bin.test.ts +++ b/src/core/data_formats/parsing/quest/bin.test.ts @@ -4,6 +4,7 @@ import { prs_decompress } from "../../compression/prs/decompress"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; import { BufferCursor } from "../../cursor/BufferCursor"; import { parse_bin, write_bin } from "./bin"; +import { Version } from "./Version"; /** * Parse a file, convert the resulting structure to BIN again and check whether the end result is equal to the original. @@ -11,7 +12,7 @@ import { parse_bin, write_bin } from "./bin"; function test_quest(path: string): void { const orig_buffer = readFileSync(path); const orig_bin = prs_decompress(new BufferCursor(orig_buffer, Endianness.Little)); - const test_buffer = write_bin(parse_bin(orig_bin)); + const test_buffer = write_bin(parse_bin(orig_bin, Version.BB)); const test_bin = new ArrayBufferCursor(test_buffer, Endianness.Little); orig_bin.seek_start(0); diff --git a/src/core/data_formats/parsing/quest/bin.ts b/src/core/data_formats/parsing/quest/bin.ts index bd55947e..78689b36 100644 --- a/src/core/data_formats/parsing/quest/bin.ts +++ b/src/core/data_formats/parsing/quest/bin.ts @@ -27,6 +27,7 @@ import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor"; import { WritableCursor } from "../../cursor/WritableCursor"; import { ResizableBuffer } from "../../ResizableBuffer"; import { LogManager } from "../../../Logger"; +import { Version } from "./Version"; const logger = LogManager.get("core/data_formats/parsing/quest/bin"); @@ -47,18 +48,35 @@ SEGMENT_PRIORITY[SegmentType.Data] = 0; export function parse_bin( cursor: Cursor, + version: Version, entry_labels: number[] = [0], lenient: boolean = false, ): BinFile { - const object_code_offset = cursor.u32(); // Always 4652 + const object_code_offset = cursor.u32(); const label_offset_table_offset = cursor.u32(); // Relative offsets const size = cursor.u32(); - cursor.seek(4); // Always seems to be 0xFFFFFFFF - const quest_id = cursor.u32(); - const language = cursor.u32(); - const quest_name = cursor.string_utf16(64, true, true); - const short_description = cursor.string_utf16(256, true, true); - const long_description = cursor.string_utf16(576, true, true); + cursor.seek(4); // Always seems to be 0xFFFFFFFF for BB. + const dc_gc_format = version === Version.DC || version === Version.GC; + let quest_id: number; + let language: number; + + if (dc_gc_format) { + language = cursor.u16(); + quest_id = cursor.u16(); + } else { + quest_id = cursor.u32(); + language = cursor.u32(); + } + + const quest_name = dc_gc_format + ? cursor.string_ascii(32, true, true) + : cursor.string_utf16(64, true, true); + const short_description = dc_gc_format + ? cursor.string_ascii(128, true, true) + : cursor.string_utf16(256, true, true); + const long_description = dc_gc_format + ? cursor.string_ascii(288, true, true) + : cursor.string_utf16(576, true, true); if (size !== cursor.size) { logger.warn(`Value ${size} in bin size field does not match actual size ${cursor.size}.`); @@ -78,7 +96,13 @@ export function parse_bin( .seek_start(object_code_offset) .take(label_offset_table_offset - object_code_offset); - const segments = parse_object_code(object_code, label_holder, entry_labels, lenient); + const segments = parse_object_code( + object_code, + label_holder, + entry_labels, + lenient, + dc_gc_format, + ); return { quest_id, @@ -220,6 +244,7 @@ function parse_object_code( label_holder: LabelHolder, entry_labels: number[], lenient: boolean, + dc_gc_format: boolean, ): Segment[] { const offset_to_segment = new Map(); @@ -229,6 +254,7 @@ function parse_object_code( entry_labels.reduce((m, l) => m.set(l, SegmentType.Instructions), new Map()), offset_to_segment, lenient, + dc_gc_format, ); const segments: Segment[] = []; @@ -333,6 +359,7 @@ function find_and_parse_segments( labels: Map, offset_to_segment: Map, lenient: boolean, + dc_gc_format: boolean, ): void { let start_segment_count: number; @@ -341,7 +368,15 @@ function find_and_parse_segments( start_segment_count = offset_to_segment.size; for (const [label, type] of labels) { - parse_segment(offset_to_segment, label_holder, cursor, label, type, lenient); + parse_segment( + offset_to_segment, + label_holder, + cursor, + label, + type, + lenient, + dc_gc_format, + ); } // Find label references. @@ -460,6 +495,7 @@ function parse_segment( label: number, type: SegmentType, lenient: boolean, + dc_gc_format: boolean, ): void { try { const info = label_holder.get_info(label); @@ -501,13 +537,14 @@ function parse_segment( labels, info.next && info.next.label, lenient, + dc_gc_format, ); break; case SegmentType.Data: parse_data_segment(offset_to_segment, cursor, end_offset, labels); break; case SegmentType.String: - parse_string_segment(offset_to_segment, cursor, end_offset, labels); + parse_string_segment(offset_to_segment, cursor, end_offset, labels, dc_gc_format); break; default: throw new Error(`Segment type ${SegmentType[type]} not implemented.`); @@ -529,6 +566,7 @@ function parse_instructions_segment( labels: number[], next_label: number | undefined, lenient: boolean, + dc_gc_format: boolean, ): void { const instructions: Instruction[] = []; @@ -559,7 +597,7 @@ function parse_instructions_segment( // Parse the arguments. try { - const args = parse_instruction_arguments(cursor, opcode); + const args = parse_instruction_arguments(cursor, opcode, dc_gc_format); instructions.push(new_instruction(opcode, args)); } catch (e) { if (lenient) { @@ -596,6 +634,7 @@ function parse_instructions_segment( next_label, SegmentType.Instructions, lenient, + dc_gc_format, ); } } @@ -622,18 +661,21 @@ function parse_string_segment( cursor: Cursor, end_offset: number, labels: number[], + dc_gc_format: boolean, ): void { const start_offset = cursor.position; const segment: StringSegment = { type: SegmentType.String, labels, - value: cursor.string_utf16(end_offset - start_offset, true, true), + value: dc_gc_format + ? cursor.string_ascii(end_offset - start_offset, true, true) + : cursor.string_utf16(end_offset - start_offset, true, true), asm: { labels: [] }, }; offset_to_segment.set(start_offset, segment); } -function parse_instruction_arguments(cursor: Cursor, opcode: Opcode): Arg[] { +function parse_instruction_arguments(cursor: Cursor, opcode: Opcode, dc_gc_format: boolean): Arg[] { const args: Arg[] = []; if (opcode.stack !== StackInteraction.Pop) { @@ -660,9 +702,12 @@ function parse_instruction_arguments(cursor: Cursor, opcode: Opcode): Arg[] { case Kind.String: { const start_pos = cursor.position; + const max_bytes = Math.min(4096, cursor.bytes_left); args.push( new_arg( - cursor.string_utf16(Math.min(4096, cursor.bytes_left), true, false), + dc_gc_format + ? cursor.string_ascii(max_bytes, true, false) + : cursor.string_utf16(max_bytes, true, false), cursor.position - start_pos, ), ); diff --git a/src/core/data_formats/parsing/quest/index.ts b/src/core/data_formats/parsing/quest/index.ts index 8ef27a27..29c7899a 100644 --- a/src/core/data_formats/parsing/quest/index.ts +++ b/src/core/data_formats/parsing/quest/index.ts @@ -20,6 +20,7 @@ import { parse_qst, QstContainedFile, write_qst } from "./qst"; import { npc_data, NpcType } from "./npc_types"; import { reinterpret_f32_as_i32, reinterpret_i32_as_f32 } from "../../../primitive_conversion"; import { LogManager } from "../../../Logger"; +import { Version } from "./Version"; const logger = LogManager.get("core/data_formats/parsing/quest"); @@ -90,6 +91,7 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u ); const bin = parse_bin( bin_decompressed, + qst.version === Version.DC || qst.version === Version.GC ? 1 : 2, extract_script_entry_points(objects, dat.npcs), lenient, ); diff --git a/src/core/data_formats/parsing/quest/qst.test.ts b/src/core/data_formats/parsing/quest/qst.test.ts index eb4d615c..65218e9c 100644 --- a/src/core/data_formats/parsing/quest/qst.test.ts +++ b/src/core/data_formats/parsing/quest/qst.test.ts @@ -1,9 +1,10 @@ import { walk_qst_files } from "../../../../../test/src/utils"; -import { parse_qst, Version, write_qst } from "./qst"; +import { parse_qst, write_qst } from "./qst"; import { Endianness } from "../../Endianness"; import { BufferCursor } from "../../cursor/BufferCursor"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; import * as fs from "fs"; +import { Version } from "./Version"; test("Parse a GC quest.", () => { const buf = fs.readFileSync("test/resources/lost_heat_sword_gc.qst"); diff --git a/src/core/data_formats/parsing/quest/qst.ts b/src/core/data_formats/parsing/quest/qst.ts index 93354e10..490b028a 100644 --- a/src/core/data_formats/parsing/quest/qst.ts +++ b/src/core/data_formats/parsing/quest/qst.ts @@ -6,6 +6,7 @@ import { WritableCursor } from "../../cursor/WritableCursor"; import { ResizableBuffer } from "../../ResizableBuffer"; import { basename, defined } from "../../../util"; import { LogManager } from "../../../Logger"; +import { Version } from "./Version"; const logger = LogManager.get("core/data_formats/parsing/quest/qst"); @@ -15,22 +16,6 @@ const ONLINE_QUEST = 0x44; const DOWNLOAD_QUEST = 0xa6; const CHUNK_BODY_SIZE = 1024; -export enum Version { - /** - * Dreamcast - */ - DC, - /** - * GameCube - */ - GC, - PC, - /** - * BlueBurst - */ - BB, -} - export type QstContainedFile = { readonly id?: number; readonly filename: string; diff --git a/src/quest_editor/scripting/disassembly.test.ts b/src/quest_editor/scripting/disassembly.test.ts index df477456..50f9054c 100644 --- a/src/quest_editor/scripting/disassembly.test.ts +++ b/src/quest_editor/scripting/disassembly.test.ts @@ -14,6 +14,7 @@ import { SegmentType, } from "./instructions"; import { OP_ARG_PUSHW, OP_RET, OP_SWITCH_JMP, OP_VA_CALL, OP_VA_END, OP_VA_START } from "./opcodes"; +import { Version } from "../../core/data_formats/parsing/quest/Version"; test("vararg instructions should be disassembled correctly", () => { const asm = disassemble([ @@ -82,7 +83,7 @@ test("va list instructions should be disassembled correctly", () => { test("assembling disassembled object code with manual stack management should result in the same IR", () => { const orig_buffer = readFileSync("test/resources/quest27_e.bin"); const orig_bytes = prs_decompress(new BufferCursor(orig_buffer, Endianness.Little)); - const bin = parse_bin(orig_bytes); + const bin = parse_bin(orig_bytes, Version.BB); const { object_code, warnings, errors } = assemble(disassemble(bin.object_code, true), true); @@ -96,7 +97,7 @@ test("assembling disassembled object code with manual stack management should re test("assembling disassembled object code with automatic stack management should result in the same IR", () => { const orig_buffer = readFileSync("test/resources/quest27_e.bin"); const orig_bytes = prs_decompress(new BufferCursor(orig_buffer, Endianness.Little)); - const bin = parse_bin(orig_bytes); + const bin = parse_bin(orig_bytes, Version.BB); const { object_code, warnings, errors } = assemble(disassemble(bin.object_code, false), false); @@ -110,7 +111,7 @@ test("assembling disassembled object code with automatic stack management should test("assembling disassembled object code with manual stack management should result in the same object code", () => { const orig_buffer = readFileSync("test/resources/quest27_e.bin"); const orig_bytes = prs_decompress(new BufferCursor(orig_buffer, Endianness.Little)); - const bin = parse_bin(orig_bytes); + const bin = parse_bin(orig_bytes, Version.BB); const { object_code, warnings, errors } = assemble(disassemble(bin.object_code, true), true); @@ -144,7 +145,7 @@ test("assembling disassembled object code with manual stack management should re test("disassembling assembled assembly code with automatic stack management should result the same assembly code", () => { const orig_buffer = readFileSync("test/resources/quest27_e.bin"); const orig_bytes = prs_decompress(new BufferCursor(orig_buffer, Endianness.Little)); - const orig_asm = disassemble(parse_bin(orig_bytes).object_code, false); + const orig_asm = disassemble(parse_bin(orig_bytes, Version.BB).object_code, false); const { object_code, warnings, errors } = assemble(orig_asm, false);