diff --git a/assets_generation/update_ephinea_data.ts b/assets_generation/update_ephinea_data.ts index d7ad44ac..c639f040 100644 --- a/assets_generation/update_ephinea_data.ts +++ b/assets_generation/update_ephinea_data.ts @@ -2,7 +2,7 @@ import { readdirSync, readFileSync, statSync, writeFileSync } from "fs"; import { ASSETS_DIR, RESOURCE_DIR } from "."; import { BufferCursor } from "../src/core/data_formats/cursor/BufferCursor"; import { ItemPmt, parse_item_pmt } from "../src/core/data_formats/parsing/itempmt"; -import { parse_quest } from "../src/core/data_formats/parsing/quest"; +import { parse_qst_to_quest } from "../src/core/data_formats/parsing/quest"; import { parse_unitxt, Unitxt } from "../src/core/data_formats/parsing/unitxt"; import { Difficulties, Difficulty, SectionId, SectionIds } from "../src/core/model"; import { update_drops_from_website } from "./update_drops_ephinea"; @@ -111,7 +111,7 @@ function process_quest_dir(path: string, quests: QuestDto[]): void { function process_quest(path: string, quests: QuestDto[]): void { try { const buf = readFileSync(path); - const q = parse_quest(new BufferCursor(buf, Endianness.Little), true); + const q = parse_qst_to_quest(new BufferCursor(buf, Endianness.Little), true); if (q) { logger.trace(`Processing quest "${q.name}".`); diff --git a/src/core/data_formats/parsing/quest/bin.test.ts b/src/core/data_formats/parsing/quest/bin.test.ts index ec5752f0..8735266b 100644 --- a/src/core/data_formats/parsing/quest/bin.test.ts +++ b/src/core/data_formats/parsing/quest/bin.test.ts @@ -12,7 +12,7 @@ import { Version } from "./Version"; 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, Version.BB)); + const test_buffer = write_bin(parse_bin(orig_bin)); 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 78689b36..0b6e7c13 100644 --- a/src/core/data_formats/parsing/quest/bin.ts +++ b/src/core/data_formats/parsing/quest/bin.ts @@ -27,7 +27,6 @@ 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"); @@ -48,15 +47,16 @@ SEGMENT_PRIORITY[SegmentType.Data] = 0; export function parse_bin( cursor: Cursor, - version: Version, entry_labels: number[] = [0], lenient: boolean = false, ): BinFile { + const dc_gc_format = cursor.u8_at(0) !== 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 for BB. - const dc_gc_format = version === Version.DC || version === Version.GC; + let quest_id: number; let language: number; diff --git a/src/core/data_formats/parsing/quest/index.test.ts b/src/core/data_formats/parsing/quest/index.test.ts index b9775cae..4c4c1f71 100644 --- a/src/core/data_formats/parsing/quest/index.test.ts +++ b/src/core/data_formats/parsing/quest/index.test.ts @@ -3,13 +3,13 @@ import { Endianness } from "../../Endianness"; import { walk_qst_files } from "../../../../../test/src/utils"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; import { BufferCursor } from "../../cursor/BufferCursor"; -import { parse_quest, write_quest_qst } from "./index"; +import { parse_qst_to_quest, write_quest_qst } from "./index"; import { ObjectType } from "./object_types"; test("parse Towards the Future", () => { const buffer = readFileSync("test/resources/quest118_e.qst"); const cursor = new BufferCursor(buffer, Endianness.Little); - const quest = parse_quest(cursor)!; + const quest = parse_qst_to_quest(cursor)!; expect(quest.name).toBe("Towards the Future"); expect(quest.short_description).toBe("Challenge the\nnew simulator."); @@ -58,9 +58,9 @@ if (process.env["RUN_ALL_TESTS"] === "true") { function round_trip_test(path: string, file_name: string, contents: Buffer): void { test(`parse_quest and write_quest_qst ${path}`, () => { - const orig_quest = parse_quest(new BufferCursor(contents, Endianness.Little))!; + const orig_quest = parse_qst_to_quest(new BufferCursor(contents, Endianness.Little))!; const test_bin = write_quest_qst(orig_quest, file_name); - const test_quest = parse_quest(new ArrayBufferCursor(test_bin, Endianness.Little))!; + const test_quest = parse_qst_to_quest(new ArrayBufferCursor(test_bin, Endianness.Little))!; expect(test_quest.name).toBe(orig_quest.name); expect(test_quest.short_description).toBe(orig_quest.short_description); diff --git a/src/core/data_formats/parsing/quest/index.ts b/src/core/data_formats/parsing/quest/index.ts index 29c7899a..3f915647 100644 --- a/src/core/data_formats/parsing/quest/index.ts +++ b/src/core/data_formats/parsing/quest/index.ts @@ -20,7 +20,6 @@ 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"); @@ -43,55 +42,19 @@ export type Quest = { readonly map_designations: Map; }; -/** - * High level parsing function that delegates to lower level parsing functions. - * - * Always delegates to parse_qst at the moment. - */ -export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | undefined { - // Extract contained .dat and .bin files. - const qst = parse_qst(cursor); - - if (!qst) { - return; - } - - let dat_file: QstContainedFile | undefined; - let bin_file: QstContainedFile | undefined; - - for (const file of qst.files) { - const file_name = file.filename.trim().toLowerCase(); - - if (file_name.endsWith(".dat")) { - dat_file = file; - } else if (file_name.endsWith(".bin")) { - bin_file = file; - } - } - - if (!dat_file) { - logger.error("File contains no DAT file."); - return; - } - - if (!bin_file) { - logger.error("File contains no BIN file."); - return; - } - - // Decompress and parse contained files. - const dat_decompressed = prs_decompress( - new ArrayBufferCursor(dat_file.data, Endianness.Little), - ); +export function parse_bin_dat_to_quest( + bin_cursor: Cursor, + dat_cursor: Cursor, + lenient: boolean = false, +): Quest | undefined { + // Decompress and parse files. + const dat_decompressed = prs_decompress(dat_cursor); const dat = parse_dat(dat_decompressed); const objects = parse_obj_data(dat.objs); - const bin_decompressed = prs_decompress( - new ArrayBufferCursor(bin_file.data, Endianness.Little), - ); + const bin_decompressed = prs_decompress(bin_cursor); const bin = parse_bin( bin_decompressed, - qst.version === Version.DC || qst.version === Version.GC ? 1 : 2, extract_script_entry_points(objects, dat.npcs), lenient, ); @@ -137,6 +100,44 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u }; } +export function parse_qst_to_quest(cursor: Cursor, lenient: boolean = false): Quest | undefined { + // Extract contained .dat and .bin files. + const qst = parse_qst(cursor); + + if (!qst) { + return; + } + + let dat_file: QstContainedFile | undefined; + let bin_file: QstContainedFile | undefined; + + for (const file of qst.files) { + const file_name = file.filename.trim().toLowerCase(); + + if (file_name.endsWith(".dat")) { + dat_file = file; + } else if (file_name.endsWith(".bin")) { + bin_file = file; + } + } + + if (!dat_file) { + logger.error("File contains no DAT file."); + return; + } + + if (!bin_file) { + logger.error("File contains no BIN file."); + return; + } + + return parse_bin_dat_to_quest( + new ArrayBufferCursor(bin_file.data, Endianness.Little), + new ArrayBufferCursor(dat_file.data, Endianness.Little), + lenient, + ); +} + export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer { const dat = write_dat({ objs: objects_to_dat_data(quest.objects), diff --git a/src/core/gui/FileButton.ts b/src/core/gui/FileButton.ts index eafe0081..dfc7293f 100644 --- a/src/core/gui/FileButton.ts +++ b/src/core/gui/FileButton.ts @@ -7,6 +7,7 @@ import { WritableProperty } from "../observable/property/WritableProperty"; export type FileButtonOptions = ControlOptions & { accept?: string; + multiple?: boolean; icon_left?: Icon; }; @@ -37,19 +38,23 @@ export class FileButton extends Control { } }; - if (options && options.accept) this.input.accept = options.accept; - const inner_element = span({ className: "core_FileButton_inner core_Button_inner", }); - if (options && options.icon_left != undefined) { - inner_element.append( - span( - { className: "core_FileButton_left core_Button_left" }, - icon(options.icon_left), - ), - ); + if (options) { + if (options.accept != undefined) this.input.accept = options.accept; + + if (options.multiple != undefined) this.input.multiple = options.multiple; + + if (options.icon_left != undefined) { + inner_element.append( + span( + { className: "core_FileButton_left core_Button_left" }, + icon(options.icon_left), + ), + ); + } } inner_element.append(span({ className: "core_Button_center" }, text)); diff --git a/src/quest_editor/controllers/QuestEditorToolBarController.ts b/src/quest_editor/controllers/QuestEditorToolBarController.ts index 9ef1311f..bfc178a1 100644 --- a/src/quest_editor/controllers/QuestEditorToolBarController.ts +++ b/src/quest_editor/controllers/QuestEditorToolBarController.ts @@ -9,12 +9,18 @@ import { Controller } from "../../core/controllers/Controller"; import { Episode } from "../../core/data_formats/parsing/quest/Episode"; import { create_new_quest } from "../stores/quest_creation"; import { read_file } from "../../core/read_file"; -import { parse_quest, write_quest_qst } from "../../core/data_formats/parsing/quest"; +import { + parse_bin_dat_to_quest, + parse_qst_to_quest, + Quest, + write_quest_qst, +} from "../../core/data_formats/parsing/quest"; import { ArrayBufferCursor } from "../../core/data_formats/cursor/ArrayBufferCursor"; import { Endianness } from "../../core/data_formats/Endianness"; import { convert_quest_from_model, convert_quest_to_model } from "../stores/model_conversion"; import { LogManager } from "../../core/Logger"; import { input } from "../../core/gui/dom"; +import { basename } from "../../core/util"; const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController"); @@ -99,9 +105,10 @@ export class QuestEditorToolBarController extends Controller { gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", () => { const input_element = input(); input_element.type = "file"; + input_element.multiple = true; input_element.onchange = () => { if (input_element.files && input_element.files.length) { - this.open_file(input_element.files[0]); + this.open_files(Array.prototype.slice.apply(input_element.files)); } }; input_element.click(); @@ -135,10 +142,30 @@ export class QuestEditorToolBarController extends Controller { this.quest_editor_store.set_current_quest(create_new_quest(this.area_store, episode)); // TODO: notify user of problems. - open_file = async (file: File): Promise => { + open_files = async (files: File[]): Promise => { try { - const buffer = await read_file(file); - const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little)); + let quest: Quest | undefined; + + const qst = files.find(f => f.name.toLowerCase().endsWith(".qst")); + + if (qst) { + const buffer = await read_file(qst); + quest = parse_qst_to_quest(new ArrayBufferCursor(buffer, Endianness.Little)); + this.quest_filename = qst.name; + } else { + const bin = files.find(f => f.name.toLowerCase().endsWith(".bin")); + const dat = files.find(f => f.name.toLowerCase().endsWith(".dat")); + + if (bin && dat) { + const bin_buffer = await read_file(bin); + const dat_buffer = await read_file(dat); + quest = parse_bin_dat_to_quest( + new ArrayBufferCursor(bin_buffer, Endianness.Little), + new ArrayBufferCursor(dat_buffer, Endianness.Little), + ); + this.quest_filename = bin.name || dat.name; + } + } if (!quest) { logger.error("Couldn't parse quest file."); @@ -147,8 +174,6 @@ export class QuestEditorToolBarController extends Controller { await this.quest_editor_store.set_current_quest( quest && convert_quest_to_model(this.area_store, quest), ); - - this.quest_filename = file.name; } catch (e) { logger.error("Couldn't read file.", e); } @@ -162,12 +187,7 @@ export class QuestEditorToolBarController extends Controller { const quest = this.quest_editor_store.current_quest.val; if (!quest) return; - let default_file_name = this.quest_filename; - - if (default_file_name) { - const ext_start = default_file_name.lastIndexOf("."); - if (ext_start !== -1) default_file_name = default_file_name.slice(0, ext_start); - } + const default_file_name = this.quest_filename && basename(this.quest_filename); let file_name = prompt("File name:", default_file_name); if (!file_name) return; diff --git a/src/quest_editor/gui/QuestEditorToolBar.ts b/src/quest_editor/gui/QuestEditorToolBar.ts index 840952f0..e21141a5 100644 --- a/src/quest_editor/gui/QuestEditorToolBar.ts +++ b/src/quest_editor/gui/QuestEditorToolBar.ts @@ -21,7 +21,8 @@ export class QuestEditorToolBar extends ToolBar { }); const open_file_button = new FileButton("Open file...", { icon_left: Icon.File, - accept: ".qst", + accept: ".bin, .dat, .qst", + multiple: true, tooltip: "Open a quest file (Ctrl-O)", }); const save_as_button = new Button({ @@ -107,11 +108,7 @@ export class QuestEditorToolBar extends ToolBar { this.disposables( new_quest_button.chosen.observe(({ value: episode }) => ctrl.create_new_quest(episode)), - open_file_button.files.observe(({ value: files }) => { - if (files.length) { - ctrl.open_file(files[0]); - } - }), + open_file_button.files.observe(({ value: files }) => ctrl.open_files(files)), save_as_button.click.observe(ctrl.save_as), save_as_button.enabled.bind_to(ctrl.can_save), diff --git a/src/quest_editor/scripting/disassembly.test.ts b/src/quest_editor/scripting/disassembly.test.ts index 50f9054c..df477456 100644 --- a/src/quest_editor/scripting/disassembly.test.ts +++ b/src/quest_editor/scripting/disassembly.test.ts @@ -14,7 +14,6 @@ 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([ @@ -83,7 +82,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, Version.BB); + const bin = parse_bin(orig_bytes); const { object_code, warnings, errors } = assemble(disassemble(bin.object_code, true), true); @@ -97,7 +96,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, Version.BB); + const bin = parse_bin(orig_bytes); const { object_code, warnings, errors } = assemble(disassemble(bin.object_code, false), false); @@ -111,7 +110,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, Version.BB); + const bin = parse_bin(orig_bytes); const { object_code, warnings, errors } = assemble(disassemble(bin.object_code, true), true); @@ -145,7 +144,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, Version.BB).object_code, false); + const orig_asm = disassemble(parse_bin(orig_bytes).object_code, false); const { object_code, warnings, errors } = assemble(orig_asm, false);