Quests can now be saved in GC format.

This commit is contained in:
Daan Vanden Bosch 2020-01-16 21:45:20 +01:00
parent 7c9a74171e
commit b276ba988e
39 changed files with 552 additions and 264 deletions

View File

@ -91,7 +91,7 @@ export class Logger {
this.log(Severity.Info, message, cause);
};
warning = (message: string, cause?: any): void => {
warn = (message: string, cause?: any): void => {
this.log(Severity.Warning, message, cause);
};

View File

@ -25,7 +25,7 @@ export function get_map_designations(
const area_id = get_register_value(cfg, inst, inst.args[0].value);
if (area_id.size() !== 1) {
logger.warning(`Couldn't determine area ID for map_designate instruction.`);
logger.warn(`Couldn't determine area ID for map_designate instruction.`);
continue;
}
@ -34,7 +34,7 @@ export function get_map_designations(
const variant_id = get_register_value(cfg, inst, variant_id_register);
if (variant_id.size() !== 1) {
logger.warning(
logger.warn(
`Couldn't determine area variant ID for map_designate instruction.`,
);
continue;

View File

@ -68,7 +68,7 @@ function find_values(
register: number,
): ValueSet {
if (++ctx.iterations > 100) {
logger.warning("Too many iterations.");
logger.warn("Too many iterations.");
return new ValueSet().set_interval(MIN_REGISTER_VALUE, MAX_REGISTER_VALUE);
}

View File

@ -58,7 +58,7 @@ function find_values(
position: number,
): ValueSet {
if (++ctx.iterations > 100) {
logger.warning("Too many iterations.");
logger.warn("Too many iterations.");
return new ValueSet().set_interval(MIN_STACK_VALUE, MAX_STACK_VALUE);
}

View File

@ -302,7 +302,7 @@ function parse_chunks(
type: NjcmChunkType.Unknown,
type_id,
});
logger.warning(`Unknown chunk type ${type_id} at offset ${chunk_start_position}.`);
logger.warn(`Unknown chunk type ${type_id} at offset ${chunk_start_position}.`);
}
cursor.seek_start(chunk_start_position + size);
@ -317,7 +317,7 @@ function parse_vertex_chunk(
flags: number,
): NjcmChunkVertex[] {
if (chunk_type_id < 32 || chunk_type_id > 50) {
logger.warning(`Unknown vertex chunk type ${chunk_type_id}.`);
logger.warn(`Unknown vertex chunk type ${chunk_type_id}.`);
return [];
}

View File

@ -58,7 +58,7 @@ export function parse_xj_model(cursor: Cursor): XjModel {
if (vertex_info_count >= 1) {
if (vertex_info_count > 1) {
logger.warning(`Vertex info count of ${vertex_info_count} was larger than expected.`);
logger.warn(`Vertex info count of ${vertex_info_count} was larger than expected.`);
}
model.vertices.push(...parse_vertex_info_table(cursor, vertex_info_table_offset));
@ -116,7 +116,7 @@ function parse_vertex_info_table(cursor: Cursor, vertex_info_table_offset: numbe
uv = cursor.vec2_f32();
break;
default:
logger.warning(`Unknown vertex type ${vertex_type} with size ${vertex_size}.`);
logger.warn(`Unknown vertex type ${vertex_type} with size ${vertex_size}.`);
break;
}

View File

@ -15,7 +15,7 @@ export function parse_prc(cursor: Cursor): Cursor {
const out = prs_decompress(prc_decrypt(key, cursor));
if (out.size !== size) {
logger.warning(
logger.warn(
`Size of decrypted, decompressed file was ${out.size} instead of expected ${size}.`,
);
}

View File

@ -0,0 +1,28 @@
import { Version } from "./Version";
export enum BinFormat {
/**
* Dreamcast/GameCube
*/
DC_GC,
/**
* Desktop
*/
PC,
/**
* BlueBurst
*/
BB,
}
export function version_to_bin_format(version: Version): BinFormat {
switch (version) {
case Version.DC:
case Version.GC:
return BinFormat.DC_GC;
case Version.PC:
return BinFormat.PC;
case Version.BB:
return BinFormat.BB;
}
}

View File

@ -7,9 +7,14 @@ export enum Version {
* GameCube
*/
GC,
/**
* Desktop
*/
PC,
/**
* BlueBurst
*/
BB,
}
export const VERSIONS: Version[] = [Version.DC, Version.GC, Version.PC, Version.BB];

View File

@ -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 { BinFormat } from "./BinFormat";
/**
* 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).bin);
const test_buffer = write_bin(parse_bin(orig_bin).bin, BinFormat.BB);
const test_bin = new ArrayBufferCursor(test_buffer, Endianness.Little);
orig_bin.seek_start(0);

View File

@ -3,9 +3,14 @@ import { Cursor } from "../../cursor/Cursor";
import { LogManager } from "../../../Logger";
import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
import { assert } from "../../../util";
import { BinFormat } from "./BinFormat";
const logger = LogManager.get("core/data_formats/parsing/quest/bin");
const DC_GC_OBJECT_CODE_OFFSET = 468;
const PC_OBJECT_CODE_OFFSET = 920;
const BB_OBJECT_CODE_OFFSET = 4652;
export type BinFile = {
readonly quest_id: number;
readonly language: number;
@ -17,47 +22,64 @@ export type BinFile = {
readonly shop_items: readonly number[];
};
export function parse_bin(cursor: Cursor): { bin: BinFile; dc_gc_format: boolean } {
export function parse_bin(cursor: Cursor): { bin: BinFile; format: BinFormat } {
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.
cursor.seek(4); // Always seems to be 0xFFFFFFFF.
const dc_gc_format = object_code_offset !== 4652;
let format: number;
switch (object_code_offset) {
case DC_GC_OBJECT_CODE_OFFSET:
format = BinFormat.DC_GC;
break;
case BB_OBJECT_CODE_OFFSET:
format = BinFormat.BB;
break;
default:
format = BinFormat.PC;
break;
}
let quest_id: number;
let language: number;
let quest_name: string;
let short_description: string;
let long_description: string;
if (dc_gc_format) {
language = cursor.u16();
if (format === BinFormat.DC_GC) {
cursor.seek(1);
language = cursor.u8();
quest_id = cursor.u16();
quest_name = cursor.string_ascii(32, true, true);
short_description = cursor.string_ascii(128, true, true);
long_description = cursor.string_ascii(288, true, true);
} else {
quest_id = cursor.u32();
language = cursor.u32();
quest_name = cursor.string_utf16(64, true, true);
short_description = cursor.string_utf16(256, true, true);
long_description = cursor.string_utf16(576, true, true);
}
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.warning(`Value ${size} in bin size field does not match actual size ${cursor.size}.`);
logger.warn(`Value ${size} in bin size field does not match actual size ${cursor.size}.`);
}
cursor.seek(4); // Skip padding.
let shop_items: number[];
const shop_items = cursor.u32_array(932);
if (format === BinFormat.BB) {
cursor.seek(4); // Skip padding.
shop_items = cursor.u32_array(932);
} else {
shop_items = [];
}
const label_offset_count = Math.floor((cursor.size - label_offset_table_offset) / 4);
cursor.seek_start(label_offset_table_offset);
const label_offsets = cursor.i32_array(label_offset_count);
const label_offsets = cursor
.seek_start(label_offset_table_offset)
.i32_array(label_offset_count);
const object_code = cursor
.seek_start(object_code_offset)
@ -74,12 +96,48 @@ export function parse_bin(cursor: Cursor): { bin: BinFile; dc_gc_format: boolean
label_offsets,
shop_items,
},
dc_gc_format,
format,
};
}
export function write_bin(bin: BinFile): ArrayBuffer {
const object_code_offset = 4652;
export function write_bin(bin: BinFile, format: BinFormat): ArrayBuffer {
assert(
bin.quest_name.length <= 32,
() => `quest_name can't be longer than 32 characters, was ${bin.quest_name.length}`,
);
assert(
bin.short_description.length <= 127,
() =>
`short_description can't be longer than 127 characters, was ${bin.short_description.length}`,
);
assert(
bin.long_description.length <= 287,
() =>
`long_description can't be longer than 287 characters, was ${bin.long_description.length}`,
);
assert(
bin.shop_items.length === 0 || format === BinFormat.BB,
"shop_items is only supported in BlueBurst quests.",
);
assert(
bin.shop_items.length <= 932,
() => `shop_items can't be larger than 932, was ${bin.shop_items.length}.`,
);
let object_code_offset: number;
switch (format) {
case BinFormat.DC_GC:
object_code_offset = DC_GC_OBJECT_CODE_OFFSET;
break;
case BinFormat.PC:
object_code_offset = PC_OBJECT_CODE_OFFSET;
break;
case BinFormat.BB:
object_code_offset = BB_OBJECT_CODE_OFFSET;
break;
}
const file_size =
object_code_offset + bin.object_code.byteLength + 4 * bin.label_offsets.length;
const buffer = new ArrayBuffer(file_size);
@ -89,27 +147,37 @@ export function write_bin(bin: BinFile): ArrayBuffer {
cursor.write_u32(object_code_offset + bin.object_code.byteLength); // Label table offset.
cursor.write_u32(file_size);
cursor.write_u32(0xffffffff);
cursor.write_u32(bin.quest_id);
cursor.write_u32(bin.language);
cursor.write_string_utf16(bin.quest_name, 64);
cursor.write_string_utf16(bin.short_description, 256);
cursor.write_string_utf16(bin.long_description, 576);
cursor.write_u32(0);
if (bin.shop_items.length > 932) {
throw new Error(`shop_items can't be larger than 932, was ${bin.shop_items.length}.`);
}
cursor.write_u32_array(bin.shop_items);
for (let i = bin.shop_items.length; i < 932; i++) {
cursor.write_u32(0);
}
while (cursor.position < object_code_offset) {
if (format === BinFormat.DC_GC) {
cursor.write_u8(0);
cursor.write_u8(bin.language);
cursor.write_u16(bin.quest_id);
cursor.write_string_ascii(bin.quest_name, 32);
cursor.write_string_ascii(bin.short_description, 128);
cursor.write_string_ascii(bin.long_description, 288);
} else {
cursor.write_u32(bin.quest_id);
cursor.write_u32(bin.language);
cursor.write_string_utf16(bin.quest_name, 64);
cursor.write_string_utf16(bin.short_description, 256);
cursor.write_string_utf16(bin.long_description, 576);
}
if (format === BinFormat.BB) {
cursor.write_u32(0);
cursor.write_u32_array(bin.shop_items);
for (let i = bin.shop_items.length; i < 932; i++) {
cursor.write_u32(0);
}
}
assert(
cursor.position === object_code_offset,
() =>
`Expected to write ${object_code_offset} bytes before object code, but wrote ${cursor.position}.`,
);
cursor.write_cursor(new ArrayBufferCursor(bin.object_code, Endianness.Little));
cursor.write_i32_array(bin.label_offsets);

View File

@ -138,7 +138,7 @@ export function parse_dat(cursor: Cursor): DatFile {
}
if (entities_cursor.bytes_left) {
logger.warning(
logger.warn(
`Read ${entities_cursor.position} bytes instead of expected ${entities_cursor.size} for entity type ${entity_type}.`,
);
}
@ -285,7 +285,7 @@ function parse_events(cursor: Cursor, area_id: number, events: DatEvent[]): void
actions_cursor.seek_start(event_actions_offset);
actions = parse_event_actions(actions_cursor);
} else {
logger.warning(`Invalid event actions offset ${event_actions_offset} for event ${id}.`);
logger.warn(`Invalid event actions offset ${event_actions_offset} for event ${id}.`);
}
events.push({
@ -300,7 +300,7 @@ function parse_events(cursor: Cursor, area_id: number, events: DatEvent[]): void
}
if (cursor.position !== actions_offset) {
logger.warning(
logger.warn(
`Read ${cursor.position - 16} bytes of event data instead of expected ${actions_offset -
16}.`,
);
@ -364,7 +364,7 @@ function parse_event_actions(cursor: Cursor): DatEventAction[] {
break;
default:
logger.warning(`Unexpected event action type ${type}.`);
logger.warn(`Unexpected event action type ${type}.`);
break outer;
}
}

View File

@ -9,7 +9,7 @@ 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_qst_to_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.");
@ -56,11 +56,22 @@ if (process.env["RUN_ALL_TESTS"] === "true") {
round_trip_test(path_2, file_name_2, buffer_2);
}
// GC quest.
round_trip_test(
"test/resources/lost_heat_sword_gc.qst",
"lost_heat_sword_gc.qst",
readFileSync("test/resources/lost_heat_sword_gc.qst"),
);
function round_trip_test(path: string, file_name: string, contents: Buffer): void {
test(`parse_quest and write_quest_qst ${path}`, () => {
const orig_quest = parse_qst_to_quest(new BufferCursor(contents, Endianness.Little))!;
const test_qst = write_quest_qst(orig_quest, file_name);
const test_quest = parse_qst_to_quest(new ArrayBufferCursor(test_qst, Endianness.Little))!;
const { quest: orig_quest, version, online } = parse_qst_to_quest(
new BufferCursor(contents, Endianness.Little),
)!;
const test_qst = write_quest_qst(orig_quest, file_name, version, online);
const { quest: test_quest } = parse_qst_to_quest(
new ArrayBufferCursor(test_qst, Endianness.Little),
)!;
expect(test_quest.name).toBe(orig_quest.name);
expect(test_quest.short_description).toBe(orig_quest.short_description);

View File

@ -17,6 +17,9 @@ import { reinterpret_f32_as_i32, reinterpret_i32_as_f32 } from "../../../primiti
import { LogManager } from "../../../Logger";
import { parse_object_code, write_object_code } from "./object_code";
import { get_map_designations } from "../../asm/data_flow_analysis/get_map_designations";
import { basename } from "../../../util";
import { version_to_bin_format } from "./BinFormat";
import { Version } from "./Version";
const logger = LogManager.get("core/data_formats/parsing/quest");
@ -46,7 +49,7 @@ export function parse_bin_dat_to_quest(
): Quest | undefined {
// Decompress and parse files.
const bin_decompressed = prs_decompress(bin_cursor);
const { bin, dc_gc_format } = parse_bin(bin_decompressed);
const { bin, format } = parse_bin(bin_decompressed);
const dat_decompressed = prs_decompress(dat_cursor);
const dat = parse_dat(dat_decompressed);
@ -61,7 +64,7 @@ export function parse_bin_dat_to_quest(
bin.label_offsets,
extract_script_entry_points(objects, dat.npcs),
lenient,
dc_gc_format,
format,
);
if (object_code.length) {
@ -82,10 +85,10 @@ export function parse_bin_dat_to_quest(
episode = get_episode(label_0_segment);
map_designations = get_map_designations(instruction_segments, label_0_segment);
} else {
logger.warning(`No instruction for label 0 found.`);
logger.warn(`No instruction for label 0 found.`);
}
} else {
logger.warning("File contains no instruction labels.");
logger.warn("File contains no instruction labels.");
}
return {
@ -105,7 +108,10 @@ export function parse_bin_dat_to_quest(
};
}
export function parse_qst_to_quest(cursor: Cursor, lenient: boolean = false): Quest | undefined {
export function parse_qst_to_quest(
cursor: Cursor,
lenient: boolean = false,
): { quest: Quest; version: Version; online: boolean } | undefined {
// Extract contained .dat and .bin files.
const qst = parse_qst(cursor);
@ -136,14 +142,21 @@ export function parse_qst_to_quest(cursor: Cursor, lenient: boolean = false): Qu
return;
}
return parse_bin_dat_to_quest(
const quest = parse_bin_dat_to_quest(
new ArrayBufferCursor(bin_file.data, Endianness.Little),
new ArrayBufferCursor(dat_file.data, Endianness.Little),
lenient,
);
return quest && { quest, version: qst.version, online: qst.online };
}
export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer {
export function write_quest_qst(
quest: Quest,
file_name: string,
version: Version,
online: boolean,
): ArrayBuffer {
const dat = write_dat({
objs: objects_to_dat_data(quest.objects),
npcs: npcs_to_dat_data(quest.npcs),
@ -151,35 +164,43 @@ export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer {
unknowns: quest.dat_unknowns,
});
const { object_code, label_offsets } = write_object_code(quest.object_code);
const { object_code, label_offsets } = write_object_code(
quest.object_code,
version_to_bin_format(version),
);
const bin = write_bin({
quest_id: quest.id,
language: quest.language,
quest_name: quest.name,
short_description: quest.short_description,
long_description: quest.long_description,
object_code,
label_offsets,
shop_items: quest.shop_items,
});
const bin = write_bin(
{
quest_id: quest.id,
language: quest.language,
quest_name: quest.name,
short_description: quest.short_description,
long_description: quest.long_description,
object_code,
label_offsets,
shop_items: quest.shop_items,
},
version_to_bin_format(version),
);
const ext_start = file_name.lastIndexOf(".");
const base_file_name =
ext_start === -1 ? file_name.slice(0, 11) : file_name.slice(0, Math.min(11, ext_start));
const base_file_name = basename(file_name).slice(0, 11);
return write_qst({
version,
online,
files: [
{
filename: base_file_name + ".dat",
id: quest.id,
filename: base_file_name + ".dat",
quest_name: quest.name,
data: prs_compress(
new ResizableBufferCursor(dat, Endianness.Little),
).array_buffer(),
},
{
filename: base_file_name + ".bin",
id: quest.id,
filename: base_file_name + ".bin",
quest_name: quest.name,
data: prs_compress(new ArrayBufferCursor(bin, Endianness.Little)).array_buffer(),
},
],
@ -195,14 +216,18 @@ function get_episode(func_0_segment: InstructionSegment): Episode {
);
if (set_episode) {
switch (set_episode.args[0].value) {
default:
const episode = set_episode.args[0].value;
switch (episode) {
case 0:
return Episode.I;
case 1:
return Episode.II;
case 2:
return Episode.IV;
default:
logger.warn(`Unknown episode ${episode} in function 0 set_episode instruction.`);
return Episode.I;
}
} else {
logger.debug("Function 0 has no set_episode instruction.");

View File

@ -19,6 +19,7 @@ import { Endianness } from "../../Endianness";
import { LogManager } from "../../../Logger";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { ResizableBuffer } from "../../ResizableBuffer";
import { BinFormat } from "./BinFormat";
const logger = LogManager.get("core/data_formats/parsing/quest/object_code");
@ -32,19 +33,20 @@ export function parse_object_code(
label_offsets: readonly number[],
entry_labels: readonly number[],
lenient: boolean,
dc_gc_format: boolean,
format: BinFormat,
): Segment[] {
return internal_parse_object_code(
new ArrayBufferCursor(object_code, Endianness.Little),
new LabelHolder(label_offsets),
entry_labels,
lenient,
dc_gc_format,
format,
);
}
export function write_object_code(
segments: readonly Segment[],
format: BinFormat,
): { object_code: ArrayBuffer; label_offsets: number[] } {
const cursor = new ResizableBufferCursor(
new ResizableBuffer(100 * segments.length),
@ -107,7 +109,11 @@ export function write_object_code(
cursor.write_u16(arg.value);
break;
case Kind.String:
cursor.write_string_utf16(arg.value, arg.size);
if (format === BinFormat.DC_GC) {
cursor.write_string_ascii(arg.value, arg.size);
} else {
cursor.write_string_utf16(arg.value, arg.size);
}
break;
case Kind.ILabelVar:
cursor.write_u8(args.length);
@ -132,8 +138,13 @@ export function write_object_code(
}
} else if (segment.type === SegmentType.String) {
// String segments should be multiples of 4 bytes.
const byte_length = 4 * Math.ceil((segment.value.length + 1) / 2);
cursor.write_string_utf16(segment.value, byte_length);
if (format === BinFormat.DC_GC) {
const byte_length = 4 * Math.ceil((segment.value.length + 1) / 4);
cursor.write_string_ascii(segment.value, byte_length);
} else {
const byte_length = 4 * Math.ceil((segment.value.length + 1) / 2);
cursor.write_string_utf16(segment.value, byte_length);
}
} else {
cursor.write_cursor(new ArrayBufferCursor(segment.data, cursor.endianness));
}
@ -153,7 +164,7 @@ function internal_parse_object_code(
label_holder: LabelHolder,
entry_labels: readonly number[],
lenient: boolean,
dc_gc_format: boolean,
format: BinFormat,
): Segment[] {
const offset_to_segment = new Map<number, Segment>();
@ -163,7 +174,7 @@ function internal_parse_object_code(
entry_labels.reduce((m, l) => m.set(l, SegmentType.Instructions), new Map()),
offset_to_segment,
lenient,
dc_gc_format,
format,
);
const segments: Segment[] = [];
@ -244,7 +255,7 @@ function internal_parse_object_code(
segment.labels.sort((a, b) => a - b);
}
} else {
logger.warning(`Label ${label} with offset ${offset} does not point to anything.`);
logger.warn(`Label ${label} with offset ${offset} does not point to anything.`);
}
}
@ -268,7 +279,7 @@ function find_and_parse_segments(
labels: Map<number, SegmentType>,
offset_to_segment: Map<number, Segment>,
lenient: boolean,
dc_gc_format: boolean,
format: BinFormat,
): void {
let start_segment_count: number;
@ -277,15 +288,7 @@ 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,
dc_gc_format,
);
parse_segment(offset_to_segment, label_holder, cursor, label, type, lenient, format);
}
// Find label references.
@ -404,13 +407,13 @@ function parse_segment(
label: number,
type: SegmentType,
lenient: boolean,
dc_gc_format: boolean,
format: BinFormat,
): void {
try {
const info = label_holder.get_info(label);
if (info == undefined) {
logger.warning(`Label ${label} is not registered in the label table.`);
logger.warn(`Label ${label} is not registered in the label table.`);
return;
}
@ -446,14 +449,14 @@ function parse_segment(
labels,
info.next && info.next.label,
lenient,
dc_gc_format,
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, dc_gc_format);
parse_string_segment(offset_to_segment, cursor, end_offset, labels, format);
break;
default:
throw new Error(`Segment type ${SegmentType[type]} not implemented.`);
@ -475,7 +478,7 @@ function parse_instructions_segment(
labels: number[],
next_label: number | undefined,
lenient: boolean,
dc_gc_format: boolean,
format: BinFormat,
): void {
const instructions: Instruction[] = [];
@ -506,7 +509,7 @@ function parse_instructions_segment(
// Parse the arguments.
try {
const args = parse_instruction_arguments(cursor, opcode, dc_gc_format);
const args = parse_instruction_arguments(cursor, opcode, format);
instructions.push(new_instruction(opcode, args));
} catch (e) {
if (lenient) {
@ -543,7 +546,7 @@ function parse_instructions_segment(
next_label,
SegmentType.Instructions,
lenient,
dc_gc_format,
format,
);
}
}
@ -570,21 +573,22 @@ function parse_string_segment(
cursor: Cursor,
end_offset: number,
labels: number[],
dc_gc_format: boolean,
format: BinFormat,
): void {
const start_offset = cursor.position;
const segment: StringSegment = {
type: SegmentType.String,
labels,
value: dc_gc_format
? cursor.string_ascii(end_offset - start_offset, true, true)
: cursor.string_utf16(end_offset - start_offset, true, true),
value:
format === BinFormat.DC_GC
? 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, dc_gc_format: boolean): Arg[] {
function parse_instruction_arguments(cursor: Cursor, opcode: Opcode, format: BinFormat): Arg[] {
const args: Arg[] = [];
if (opcode.stack !== StackInteraction.Pop) {
@ -614,7 +618,7 @@ function parse_instruction_arguments(cursor: Cursor, opcode: Opcode, dc_gc_forma
const max_bytes = Math.min(4096, cursor.bytes_left);
args.push(
new_arg(
dc_gc_format
format === BinFormat.DC_GC
? cursor.string_ascii(max_bytes, true, false)
: cursor.string_utf16(max_bytes, true, false),
cursor.position - start_pos,

View File

@ -4,17 +4,27 @@ import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { WritableCursor } from "../../cursor/WritableCursor";
import { ResizableBuffer } from "../../ResizableBuffer";
import { basename, defined } from "../../../util";
import { assert, basename, defined } from "../../../util";
import { LogManager } from "../../../Logger";
import { Version } from "./Version";
const logger = LogManager.get("core/data_formats/parsing/quest/qst");
// .qst format
const DC_GC_PC_HEADER_SIZE = 60;
const BB_HEADER_SIZE = 88;
const PC_GC_HEADER_SIZE = 60;
const ONLINE_QUEST = 0x44;
const DOWNLOAD_QUEST = 0xa6;
// Chunks
const CHUNK_BODY_SIZE = 1024;
const DC_GC_PC_CHUNK_HEADER_SIZE = 20;
const DC_GC_PC_CHUNK_TRAILER_SIZE = 4;
const DC_GC_PC_CHUNK_SIZE =
CHUNK_BODY_SIZE + DC_GC_PC_CHUNK_HEADER_SIZE + DC_GC_PC_CHUNK_TRAILER_SIZE;
const BB_CHUNK_HEADER_SIZE = 24;
const BB_CHUNK_TRAILER_SIZE = 8;
const BB_CHUNK_SIZE = CHUNK_BODY_SIZE + BB_CHUNK_HEADER_SIZE + BB_CHUNK_TRAILER_SIZE;
export type QstContainedFile = {
readonly id?: number;
@ -23,7 +33,7 @@ export type QstContainedFile = {
readonly data: ArrayBuffer;
};
export type ParseQstResult = {
export type QstContent = {
readonly version: Version;
readonly online: boolean;
readonly files: readonly QstContainedFile[];
@ -33,8 +43,8 @@ export type ParseQstResult = {
* Low level parsing function for .qst files.
* Can only read the Blue Burst format.
*/
export function parse_qst(cursor: Cursor): ParseQstResult | undefined {
// A .qst file contains two 88-byte headers that describe the embedded .dat and .bin files.
export function parse_qst(cursor: Cursor): QstContent | undefined {
// A .qst file contains two headers that describe the embedded .dat and .bin files.
// Read headers and contained files.
const headers = parse_headers(cursor);
@ -52,7 +62,7 @@ export function parse_qst(cursor: Cursor): ParseQstResult | undefined {
if (version != undefined && header.version !== version) {
logger.error(
`Corrupt .qst file, header version ${Version[header.version]} for file ${
header.file_name
header.filename
} doesn't match the previous header's version ${Version[version]}.`,
);
return undefined;
@ -62,7 +72,7 @@ export function parse_qst(cursor: Cursor): ParseQstResult | undefined {
logger.error(
`Corrupt .qst file, header type ${
header.online ? '"online"' : '"download"'
} for file ${header.file_name} doesn't match the previous header's type ${
} for file ${header.filename} doesn't match the previous header's type ${
online ? '"online"' : '"download"'
}.`,
);
@ -76,7 +86,7 @@ export function parse_qst(cursor: Cursor): ParseQstResult | undefined {
defined(version, "version");
defined(online, "online");
const files = parse_files(cursor, version, new Map(headers.map(h => [h.file_name, h])));
const files = parse_files(cursor, version, new Map(headers.map(h => [h.filename, h])));
return {
version,
@ -85,31 +95,33 @@ export function parse_qst(cursor: Cursor): ParseQstResult | undefined {
};
}
export type QstContainedFileParam = {
readonly id?: number;
readonly filename: string;
readonly quest_name?: string;
readonly data: ArrayBuffer;
};
export function write_qst({ version, online, files }: QstContent): ArrayBuffer {
let file_header_size: number;
let chunk_size: number;
export type WriteQstParams = {
readonly version?: Version;
readonly files: readonly QstContainedFileParam[];
};
switch (version) {
case Version.DC:
case Version.GC:
case Version.PC:
file_header_size = DC_GC_PC_HEADER_SIZE;
chunk_size = DC_GC_PC_CHUNK_SIZE;
break;
case Version.BB:
file_header_size = BB_HEADER_SIZE;
chunk_size = BB_CHUNK_SIZE;
break;
}
/**
* Always uses Blue Burst format.
*/
export function write_qst(params: WriteQstParams): ArrayBuffer {
const files = params.files;
const total_size = files
.map(f => 88 + Math.ceil(f.data.byteLength / 1024) * 1056)
.map(f => file_header_size + Math.ceil(f.data.byteLength / CHUNK_BODY_SIZE) * chunk_size)
.reduce((a, b) => a + b);
const buffer = new ArrayBuffer(total_size);
const cursor = new ArrayBufferCursor(buffer, Endianness.Little);
write_file_headers(cursor, files);
write_file_chunks(cursor, files);
write_file_headers(cursor, files, version, online, file_header_size);
write_file_chunks(cursor, files, version);
if (cursor.position !== total_size) {
throw new Error(`Expected a final file size of ${total_size}, but got ${cursor.position}.`);
@ -123,7 +135,7 @@ type QstHeader = {
readonly online: boolean;
readonly quest_id: number;
readonly name: string;
readonly file_name: string;
readonly filename: string;
readonly size: number;
};
@ -131,13 +143,13 @@ function parse_headers(cursor: Cursor): QstHeader[] {
const headers: QstHeader[] = [];
let prev_quest_id: number | undefined = undefined;
let prev_file_name: string | undefined = undefined;
let prev_filename: string | undefined = undefined;
// .qst files should have two headers, some malformed files have more.
for (let i = 0; i < 4; ++i) {
// Detect version and whether it's an online or download quest.
let version;
let online;
let version: Version;
let online: boolean;
const version_a = cursor.u8();
cursor.seek(1);
@ -147,10 +159,10 @@ function parse_headers(cursor: Cursor): QstHeader[] {
if (version_a === BB_HEADER_SIZE && version_b === ONLINE_QUEST) {
version = Version.BB;
online = true;
} else if (version_a === PC_GC_HEADER_SIZE && version_b === ONLINE_QUEST) {
} else if (version_a === DC_GC_PC_HEADER_SIZE && version_b === ONLINE_QUEST) {
version = Version.PC;
online = true;
} else if (version_b === PC_GC_HEADER_SIZE) {
} else if (version_b === DC_GC_PC_HEADER_SIZE) {
const pos = cursor.position;
cursor.seek(35);
@ -177,7 +189,7 @@ function parse_headers(cursor: Cursor): QstHeader[] {
let header_size;
let quest_id: number;
let name: string;
let file_name: string;
let filename: string;
let size: number;
switch (version) {
@ -187,7 +199,8 @@ function parse_headers(cursor: Cursor): QstHeader[] {
header_size = cursor.u16();
name = cursor.string_ascii(32, true, true);
cursor.seek(3);
file_name = cursor.string_ascii(16, true, true);
filename = cursor.string_ascii(16, true, true);
cursor.seek(1);
size = cursor.u32();
break;
@ -197,7 +210,7 @@ function parse_headers(cursor: Cursor): QstHeader[] {
header_size = cursor.u16();
name = cursor.string_ascii(32, true, true);
cursor.seek(4);
file_name = cursor.string_ascii(16, true, true);
filename = cursor.string_ascii(16, true, true);
size = cursor.u32();
break;
@ -207,7 +220,7 @@ function parse_headers(cursor: Cursor): QstHeader[] {
quest_id = cursor.u8();
name = cursor.string_ascii(32, true, true);
cursor.seek(4);
file_name = cursor.string_ascii(16, true, true);
filename = cursor.string_ascii(16, true, true);
size = cursor.u32();
break;
@ -216,7 +229,7 @@ function parse_headers(cursor: Cursor): QstHeader[] {
cursor.seek(2); // Skip online/download.
quest_id = cursor.u16();
cursor.seek(38);
file_name = cursor.string_ascii(16, true, true);
filename = cursor.string_ascii(16, true, true);
size = cursor.u32();
name = cursor.string_ascii(24, true, true);
break;
@ -226,22 +239,22 @@ function parse_headers(cursor: Cursor): QstHeader[] {
// Some malformed .qst files have extra headers.
if (
prev_quest_id != undefined &&
prev_file_name != undefined &&
(quest_id !== prev_quest_id || basename(file_name) !== basename(prev_file_name))
prev_filename != undefined &&
(quest_id !== prev_quest_id || basename(filename) !== basename(prev_filename))
) {
cursor.seek(-header_size);
break;
}
prev_quest_id = quest_id;
prev_file_name = file_name;
prev_filename = filename;
headers.push({
version,
online,
quest_id,
name,
file_name,
filename,
size,
});
}
@ -273,13 +286,13 @@ function parse_files(
case Version.DC:
case Version.GC:
case Version.PC:
chunk_size = CHUNK_BODY_SIZE + 24;
trailer_size = 4;
chunk_size = DC_GC_PC_CHUNK_SIZE;
trailer_size = DC_GC_PC_CHUNK_TRAILER_SIZE;
break;
case Version.BB:
chunk_size = CHUNK_BODY_SIZE + 32;
trailer_size = 8;
chunk_size = BB_CHUNK_SIZE;
trailer_size = BB_CHUNK_TRAILER_SIZE;
break;
}
@ -317,7 +330,7 @@ function parse_files(
name: file_name,
expected_size: header?.size,
cursor: new ResizableBufferCursor(
new ResizableBuffer(header?.size ?? 10 * 1024),
new ResizableBuffer(header?.size ?? 10 * CHUNK_BODY_SIZE),
Endianness.Little,
),
chunk_nos: new Set(),
@ -326,7 +339,7 @@ function parse_files(
}
if (file.chunk_nos.has(chunk_no)) {
logger.warning(
logger.warn(
`File chunk number ${chunk_no} of file ${file_name} was already encountered, overwriting previous chunk.`,
);
} else {
@ -338,7 +351,7 @@ function parse_files(
cursor.seek(-CHUNK_BODY_SIZE - 4);
if (size > CHUNK_BODY_SIZE) {
logger.warning(
logger.warn(
`Data segment size of ${size} is larger than expected maximum size, reading just ${CHUNK_BODY_SIZE} bytes.`,
);
size = CHUNK_BODY_SIZE;
@ -361,7 +374,7 @@ function parse_files(
}
if (cursor.bytes_left) {
logger.warning(`${cursor.bytes_left} Bytes left in file.`);
logger.warn(`${cursor.bytes_left} Bytes left in file.`);
}
for (const file of files.values()) {
@ -371,7 +384,7 @@ function parse_files(
// Check whether the expected size was correct.
if (file.expected_size != null && file.cursor.size !== file.expected_size) {
logger.warning(
logger.warn(
`File ${file.name} has an actual size of ${file.cursor.size} instead of the expected size ${file.expected_size}.`,
);
}
@ -382,7 +395,7 @@ function parse_files(
for (let chunk_no = 0; chunk_no < expected_chunk_count; ++chunk_no) {
if (!file.chunk_nos.has(chunk_no)) {
logger.warning(`File ${file.name} is missing chunk ${chunk_no}.`);
logger.warn(`File ${file.name} is missing chunk ${chunk_no}.`);
}
}
}
@ -402,49 +415,93 @@ function parse_files(
return contained_files;
}
function write_file_headers(cursor: WritableCursor, files: readonly QstContainedFileParam[]): void {
function write_file_headers(
cursor: WritableCursor,
files: readonly QstContainedFile[],
version: Version,
online: boolean,
header_size: number,
): void {
let max_id: number;
let max_quest_name_length: number;
if (version === Version.BB) {
max_id = 0xffff;
max_quest_name_length = 23;
} else {
max_id = 0xff;
max_quest_name_length = 31;
}
for (const file of files) {
if (file.filename.length > 15) {
throw new Error(`File ${file.filename} has a name longer than 15 characters.`);
assert(
file.id == undefined || (0 <= file.id && file.id <= max_id),
() => `Quest ID should be between 0 and ${max_id}, inclusive.`,
);
assert(
file.quest_name == undefined || file.quest_name.length <= max_quest_name_length,
() =>
`File ${file.filename} has a quest name longer than ${max_quest_name_length} characters (${file.quest_name}).`,
);
assert(
file.filename.length <= 15,
() => `File ${file.filename} has a filename longer than 15 characters.`,
);
switch (version) {
case Version.DC:
cursor.write_u8(online ? ONLINE_QUEST : DOWNLOAD_QUEST);
cursor.write_u8(file.id ?? 0);
cursor.write_u16(header_size);
cursor.write_string_ascii(file.quest_name ?? file.filename, 32);
cursor.write_u8(0);
cursor.write_u8(0);
cursor.write_u8(0);
cursor.write_string_ascii(file.filename, 16);
cursor.write_u8(0);
cursor.write_u32(file.data.byteLength);
break;
case Version.GC:
cursor.write_u8(online ? ONLINE_QUEST : DOWNLOAD_QUEST);
cursor.write_u8(file.id ?? 0);
cursor.write_u16(header_size);
cursor.write_string_ascii(file.quest_name ?? file.filename, 32);
cursor.write_u32(0);
cursor.write_string_ascii(file.filename, 16);
cursor.write_u32(file.data.byteLength);
break;
case Version.PC:
cursor.write_u16(header_size);
cursor.write_u8(online ? ONLINE_QUEST : DOWNLOAD_QUEST);
cursor.write_u8(file.id ?? 0);
cursor.write_string_ascii(file.quest_name ?? file.filename, 32);
cursor.write_u32(0);
cursor.write_string_ascii(file.filename, 16);
cursor.write_u32(file.data.byteLength);
break;
case Version.BB:
cursor.write_u16(header_size);
cursor.write_u16(online ? ONLINE_QUEST : DOWNLOAD_QUEST);
cursor.write_u16(file.id ?? 0);
for (let i = 0; i < 38; i++) cursor.write_u8(0);
cursor.write_string_ascii(file.filename, 16);
cursor.write_u32(file.data.byteLength);
cursor.write_string_ascii(file.quest_name ?? file.filename, 24);
break;
}
cursor.write_u16(88); // Header size.
cursor.write_u16(0x44); // Magic number.
cursor.write_u16(file.id || 0);
for (let i = 0; i < 38; ++i) {
cursor.write_u8(0);
}
cursor.write_string_ascii(file.filename, 16);
cursor.write_u32(file.data.byteLength);
let file_name_2: string;
if (file.quest_name == null) {
// Not sure this makes sense.
const dot_pos = file.filename.lastIndexOf(".");
file_name_2 =
dot_pos === -1
? file.filename + "_j"
: file.filename.slice(0, dot_pos) + "_j" + file.filename.slice(dot_pos);
} else {
file_name_2 = file.quest_name;
}
if (file_name_2.length > 24) {
throw Error(
`File ${file.filename} has a file_name_2 length (${file_name_2}) longer than 24 characters.`,
);
}
cursor.write_string_ascii(file_name_2, 24);
}
}
function write_file_chunks(cursor: WritableCursor, files: readonly QstContainedFileParam[]): void {
// Files are interleaved in 1056 byte chunks.
// Each chunk has a 24 byte header, 1024 byte data segment and an 8 byte trailer.
function write_file_chunks(
cursor: WritableCursor,
files: readonly QstContainedFile[],
version: Version,
): void {
// Files are interleaved in chunks. Each chunk has a header, fixed-size data segment and a
// trailer.
const files_to_chunk = files.map(file => ({
no: 0,
data: new ArrayBufferCursor(file.data, Endianness.Little),
@ -461,6 +518,7 @@ function write_file_chunks(cursor: WritableCursor, files: readonly QstContainedF
file_to_chunk.data,
file_to_chunk.no++,
file_to_chunk.name,
version,
)
) {
done++;
@ -470,7 +528,7 @@ function write_file_chunks(cursor: WritableCursor, files: readonly QstContainedF
}
for (const file_to_chunk of files_to_chunk) {
const expected_chunks = Math.ceil(file_to_chunk.data.size / 1024);
const expected_chunks = Math.ceil(file_to_chunk.data.size / CHUNK_BODY_SIZE);
if (file_to_chunk.no !== expected_chunks) {
throw new Error(
@ -481,29 +539,51 @@ function write_file_chunks(cursor: WritableCursor, files: readonly QstContainedF
}
/**
* @returns true if there are bytes left to write in data, false otherwise.
* @returns true if there are bytes left to write in `data`, false otherwise.
*/
function write_file_chunk(
cursor: WritableCursor,
data: Cursor,
chunk_no: number,
name: string,
version: Version,
): boolean {
cursor.write_u8_array([28, 4, 19, 0]);
cursor.write_u8(chunk_no);
cursor.write_u8_array([0, 0, 0]);
switch (version) {
case Version.DC:
case Version.GC:
cursor.write_u8(0);
cursor.write_u8(chunk_no);
cursor.write_u16(0);
break;
case Version.PC:
cursor.write_u8(0);
cursor.write_u8(0);
cursor.write_u8(0);
cursor.write_u8(chunk_no);
break;
case Version.BB:
cursor.write_u8_array([28, 4, 19, 0]);
cursor.write_u32(chunk_no);
break;
}
cursor.write_string_ascii(name, 16);
const size = Math.min(1024, data.bytes_left);
const size = Math.min(CHUNK_BODY_SIZE, data.bytes_left);
cursor.write_cursor(data.take(size));
// Padding.
for (let i = size; i < 1024; ++i) {
for (let i = size; i < CHUNK_BODY_SIZE; ++i) {
cursor.write_u8(0);
}
cursor.write_u32(size);
cursor.write_u32(0);
if (version === Version.BB) {
cursor.write_u32(0);
}
return data.bytes_left > 0;
}

View File

@ -15,7 +15,7 @@ export function parse_rlc(cursor: Cursor): Cursor[] {
const marker = cursor.string_ascii(16, true, true);
if (marker !== MARKER) {
logger.warning(`First 16 bytes where "${marker}" instead of expected "${MARKER}".`);
logger.warn(`First 16 bytes where "${marker}" instead of expected "${MARKER}".`);
}
const table_size = cursor.u32();

View File

@ -27,8 +27,7 @@
}
.core_Dialog_body {
user-select: text;
overflow: auto;
flex: 1;
margin: 4px 0;
}

View File

@ -134,6 +134,7 @@ export class Menu<T> extends Widget {
case "Enter":
evt.preventDefault();
evt.stopPropagation();
this.select_hovered();
break;
}

View File

@ -2,7 +2,7 @@ import { Dialog } from "./Dialog";
import { Button } from "./Button";
import { Result } from "../Result";
import { is_property, Property } from "../observable/property/Property";
import { li, ul } from "./dom";
import { div, li, ul } from "./dom";
import { property } from "../observable";
import { WidgetOptions } from "./Widget";
@ -75,7 +75,15 @@ export class ResultDialog extends Dialog {
}
function create_result_body(result: Result<unknown>): HTMLElement {
const body = ul(...result.problems.map(problem => li(problem.ui_message)));
body.style.cursor = "text";
const body = div();
body.style.overflow = "auto";
body.style.userSelect = "text";
body.style.height = "100%";
body.style.maxHeight = "400px"; // Workaround for chrome bug.
const list_element = ul(...result.problems.map(problem => li(problem.ui_message)));
list_element.style.cursor = "text";
body.append(list_element);
return body;
}

View File

@ -109,6 +109,7 @@ export class Select<T> extends LabelledControl {
case "Enter":
case " ":
evt.preventDefault();
evt.stopPropagation();
this.just_opened = !this.menu.visible.val;
this.menu.visible.val = true;
this.menu.focus();

View File

@ -168,7 +168,7 @@ export class Table<T> extends Widget {
if (column.tooltip) cell.title = column.tooltip(value);
} catch (e) {
logger.warning(
logger.warn(
`Error while rendering cell for index ${index}, column ${i}.`,
e,
);

View File

@ -391,7 +391,7 @@ export function bind_children_to<T>(
if (child_element) {
child_element.remove();
} else {
logger.warning(
logger.warn(
`Expected an element for removal at child index ${
change.index
} of ${node_to_string(element)} (child count: ${element.childElementCount}).`,

View File

@ -89,7 +89,7 @@ export class Disposer implements Disposable {
try {
disposable.dispose();
} catch (e) {
logger.warning("Error while disposing.", e);
logger.warn("Error while disposing.", e);
}
}
}

View File

@ -60,7 +60,7 @@ export class UndoStack implements Undo {
this.index.update(i => i - 1);
this.stack.get(this.index.val).undo();
} catch (e) {
logger.warning("Error while undoing action.", e);
logger.warn("Error while undoing action.", e);
} finally {
this.undoing_or_redoing = false;
}
@ -78,7 +78,7 @@ export class UndoStack implements Undo {
this.stack.get(this.index.val).redo();
this.index.update(i => i + 1);
} catch (e) {
logger.warning("Error while redoing action.", e);
logger.warn("Error while redoing action.", e);
} finally {
this.undoing_or_redoing = false;
}

View File

@ -64,7 +64,7 @@ export function map_get_or_put<K, V>(map: Map<K, V>, key: K, get_default: () =>
export function basename(filename: string): string {
const dot_idx = filename.lastIndexOf(".");
// < 0 means filenames doesn't contain any "."
// < 0 means filename doesn't contain any "."
// also skip index 0 because that would mean the basename is empty
if (dot_idx > 1) {
return filename.slice(0, dot_idx);
@ -76,7 +76,7 @@ export function basename(filename: string): string {
export function filename_extension(filename: string): string {
const dot_idx = filename.lastIndexOf(".");
// < 0 means filenames doesn't contain any "."
// < 0 means filename doesn't contain any "."
// also skip index 0 because that would mean the basename is empty
if (dot_idx > 1) {
return filename.slice(dot_idx + 1);

View File

@ -93,7 +93,7 @@ function create_loader(
const npc_type = (NpcType as any)[drop_dto.enemy];
if (!npc_type) {
logger.warning(
logger.warn(
`Couldn't determine NpcType of episode ${drop_dto.episode} ${drop_dto.enemy}.`,
);
continue;
@ -103,14 +103,14 @@ function create_loader(
const item_type = item_type_store.get_by_id(drop_dto.item_type_id);
if (!item_type) {
logger.warning(`Couldn't find item kind ${drop_dto.item_type_id}.`);
logger.warn(`Couldn't find item kind ${drop_dto.item_type_id}.`);
continue;
}
const section_id = (SectionId as any)[drop_dto.section_id];
if (section_id == null) {
logger.warning(`Couldn't find section ID ${drop_dto.section_id}.`);
logger.warn(`Couldn't find section ID ${drop_dto.section_id}.`);
continue;
}

View File

@ -317,7 +317,7 @@ export class QuestRunner {
},
warning: (msg: string, inst_ptr?: InstructionPointer): void => {
this.logger.warning(message_with_inst_ptr(msg, inst_ptr));
this.logger.warn(message_with_inst_ptr(msg, inst_ptr));
},
error: (err: Error, inst_ptr?: InstructionPointer): void => {

View File

@ -20,6 +20,7 @@ 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 { basename } from "../../core/util";
import { Version } from "../../core/data_formats/parsing/quest/Version";
const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController");
@ -28,6 +29,7 @@ export type AreaAndLabel = { readonly area: AreaModel; readonly label: string };
export class QuestEditorToolBarController extends Controller {
private _save_as_dialog_visible = property(false);
private _filename = property("");
private _version = property(Version.BB);
readonly vm_feature_active: boolean;
readonly areas: Property<readonly AreaAndLabel[]>;
@ -41,6 +43,7 @@ export class QuestEditorToolBarController extends Controller {
readonly can_stop: Property<boolean>;
readonly save_as_dialog_visible: Property<boolean> = this._save_as_dialog_visible;
readonly filename: Property<string> = this._filename;
readonly version: Property<Version> = this._version;
constructor(
gui_store: GuiStore,
@ -100,8 +103,6 @@ export class QuestEditorToolBarController extends Controller {
this.can_stop = quest_editor_store.quest_runner.running;
this.disposables(
quest_editor_store.current_quest.observe(() => this.set_filename("")),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-O", async () => {
const files = await open_files({ accept: ".bin, .dat, .qst", multiple: true });
this.parse_files(files);
@ -131,8 +132,11 @@ export class QuestEditorToolBarController extends Controller {
);
}
create_new_quest = async (episode: Episode): Promise<void> =>
create_new_quest = async (episode: Episode): Promise<void> => {
this.set_filename("");
this.set_version(Version.BB);
this.quest_editor_store.set_current_quest(create_new_quest(this.area_store, episode));
};
// TODO: notify user of problems.
parse_files = async (files: File[]): Promise<void> => {
@ -145,7 +149,15 @@ export class QuestEditorToolBarController extends Controller {
if (qst) {
const buffer = await read_file(qst);
quest = parse_qst_to_quest(new ArrayBufferCursor(buffer, Endianness.Little));
const parse_result = parse_qst_to_quest(
new ArrayBufferCursor(buffer, Endianness.Little),
);
if (parse_result) {
quest = parse_result.quest;
this.set_version(parse_result.version);
}
this.set_filename(basename(qst.name));
if (!quest) {
@ -192,8 +204,11 @@ export class QuestEditorToolBarController extends Controller {
const quest = this.quest_editor_store.current_quest.val;
if (!quest) return;
const format = this.version.val;
if (format === undefined) return;
let filename = this.filename.val;
const buffer = write_quest_qst(convert_quest_from_model(quest), filename);
const buffer = write_quest_qst(convert_quest_from_model(quest), filename, format, true);
if (!filename.endsWith(".qst")) {
filename += ".qst";
@ -218,6 +233,10 @@ export class QuestEditorToolBarController extends Controller {
this._filename.val = filename;
};
set_version = (version: Version): void => {
this._version.val = version;
};
debug = (): void => {
const quest = this.quest_editor_store.current_quest.val;

View File

@ -105,7 +105,7 @@ export class EventSubGraphView extends View {
const data = this.event_gui_data.get(event);
if (!data) {
logger.warning(`No GUI data for event ${event.id}.`);
logger.warn(`No GUI data for event ${event.id}.`);
continue;
}
@ -119,7 +119,7 @@ export class EventSubGraphView extends View {
const child_data = this.event_gui_data.get(child);
if (!child_data) {
logger.warning(`No GUI data for child event ${child.id}.`);
logger.warn(`No GUI data for child event ${child.id}.`);
continue;
}

View File

@ -1,5 +1,11 @@
.quest_editor_QuestEditorToolBarView_save_as_dialog_content {
display: grid;
grid-template-columns: 100px max-content;
grid-column-gap: 4px;
grid-row-gap: 4px;
align-items: center;
}
.quest_editor_QuestEditorToolBarView_save_as_dialog_content .core_Input {
margin: 1px;
}

View File

@ -14,6 +14,7 @@ import { View } from "../../core/gui/View";
import { Dialog } from "../../core/gui/Dialog";
import { TextInput } from "../../core/gui/TextInput";
import "./QuestEditorToolBarView.css";
import { Version, VERSIONS } from "../../core/data_formats/parsing/quest/Version";
export class QuestEditorToolBarView extends View {
private readonly toolbar: ToolBar;
@ -123,7 +124,28 @@ export class QuestEditorToolBarView extends View {
this.toolbar = this.disposable(new ToolBar(...children));
// "Save As" dialog.
const filename_input = this.disposable(new TextInput("", { label: "File name:" }));
const filename_input = this.disposable(
new TextInput(ctrl.filename.val, { label: "File name:" }),
);
const version_select = this.disposable(
new Select({
label: "Version:",
items: VERSIONS,
selected: ctrl.version,
to_label: version => {
switch (version) {
case Version.DC:
return "Dreamcast";
case Version.GC:
return "GameCube";
case Version.PC:
return "PC";
case Version.BB:
return "BlueBurst";
}
},
}),
);
const save_button = this.disposable(new Button({ text: "Save" }));
const cancel_button = this.disposable(new Button({ text: "Cancel" }));
@ -135,6 +157,8 @@ export class QuestEditorToolBarView extends View {
{ className: "quest_editor_QuestEditorToolBarView_save_as_dialog_content" },
filename_input.label!.element,
filename_input.element,
version_select.label!.element,
version_select.element,
),
footer: [save_button.element, cancel_button.element],
}),
@ -156,8 +180,15 @@ export class QuestEditorToolBarView extends View {
save_as_dialog.ondismiss.observe(ctrl.dismiss_save_as_dialog),
filename_input.value.bind_to(ctrl.filename),
filename_input.value.observe(({ value }) => ctrl.set_filename(value)),
version_select.selected.observe(({ value }) => {
if (value != undefined) {
ctrl.set_version(value);
}
}),
save_button.onclick.observe(ctrl.save_as),
cancel_button.onclick.observe(ctrl.dismiss_save_as_dialog),

View File

@ -244,7 +244,7 @@ export class QuestEditorView extends ResizableView {
return gl;
}
} catch (e) {
logger.warning("Couldn't instantiate golden layout with persisted layout.", e);
logger.warn("Couldn't instantiate golden layout with persisted layout.", e);
}
logger.info("Instantiating golden layout with default layout.");

View File

@ -59,12 +59,12 @@ export class EntityAssetLoader implements Disposable {
if (nj_objects.success && nj_objects.value.length) {
return ninja_object_to_buffer_geometry(nj_objects.value[0]);
} else {
logger.warning(`Couldn't parse ${url} for ${entity_type_to_string(type)}.`);
logger.warn(`Couldn't parse ${url} for ${entity_type_to_string(type)}.`);
return DEFAULT_ENTITY;
}
})
.catch(e => {
logger.warning(
logger.warn(
`Couldn't load geometry file for ${entity_type_to_string(type)}.`,
e,
);
@ -82,7 +82,7 @@ export class EntityAssetLoader implements Disposable {
return xvm.success ? xvm_to_textures(xvm.value) : [];
})
.catch(e => {
logger.warning(
logger.warn(
`Couldn't load texture file for ${entity_type_to_string(type)}.`,
e,
);

View File

@ -274,7 +274,7 @@ export class QuestModel {
try {
variants.set(area_id, this.area_store.get_variant(this.episode, area_id, 0));
} catch (e) {
logger.warning(e);
logger.warn(e);
}
}
@ -285,7 +285,7 @@ export class QuestModel {
this.area_store.get_variant(this.episode, area_id, variant_id),
);
} catch (e) {
logger.warning(e);
logger.warn(e);
}
}

View File

@ -25,6 +25,7 @@ import {
parse_object_code,
write_object_code,
} from "../../core/data_formats/parsing/quest/object_code";
import { BinFormat } from "../../core/data_formats/parsing/quest/BinFormat";
test("vararg instructions should be disassembled correctly", () => {
const asm = disassemble([
@ -99,7 +100,7 @@ test("assembling disassembled object code with manual stack management should re
bin.label_offsets,
[0],
false,
false,
BinFormat.BB,
);
const { object_code, warnings, errors } = assemble(disassemble(orig_object_code, true), true);
@ -120,7 +121,7 @@ test("assembling disassembled object code with automatic stack management should
bin.label_offsets,
[0],
false,
false,
BinFormat.BB,
);
const { object_code, warnings, errors } = assemble(disassemble(orig_object_code, false), false);
@ -135,13 +136,13 @@ 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, format } = parse_bin(orig_bytes);
const orig_object_code = parse_object_code(
bin.object_code,
bin.label_offsets,
[0],
false,
false,
BinFormat.BB,
);
const { object_code, warnings, errors } = assemble(disassemble(orig_object_code, true), true);
@ -150,7 +151,7 @@ test("assembling disassembled object code with manual stack management should re
expect(warnings).toEqual([]);
const test_bytes = new ArrayBufferCursor(
write_bin({ ...bin, ...write_object_code(object_code).object_code }),
write_bin({ ...bin, ...write_object_code(object_code, format).object_code }, BinFormat.BB),
Endianness.Little,
);
@ -185,7 +186,7 @@ test("disassembling assembled assembly code with automatic stack management shou
bin.label_offsets,
[0],
false,
false,
BinFormat.BB,
);
const orig_asm = disassemble(orig_object_code, false);

View File

@ -49,44 +49,44 @@ export interface VirtualMachineIO
export class DefaultVirtualMachineIO implements VirtualMachineIO {
map_designate(area_id: number, area_variant_id: number): void {
logger.warning(`bb_map_designate(${area_id}, ${area_variant_id})`);
logger.warn(`bb_map_designate(${area_id}, ${area_variant_id})`);
}
set_floor_handler(area_id: number, label: number): void {
logger.warning(`set_floor_handler(${area_id}, ${label})`);
logger.warn(`set_floor_handler(${area_id}, ${label})`);
}
window_msg(msg: string): void {
logger.warning(`window_msg("${msg}")`);
logger.warn(`window_msg("${msg}")`);
}
message(msg: string): void {
logger.warning(`message("${msg}")`);
logger.warn(`message("${msg}")`);
}
add_msg(msg: string): void {
logger.warning(`add_msg("${msg}")`);
logger.warn(`add_msg("${msg}")`);
}
winend(): void {
logger.warning("winend");
logger.warn("winend");
}
p_dead_v3(player_slot: number): boolean {
logger.warning(`p_dead_v3(${player_slot})`);
logger.warn(`p_dead_v3(${player_slot})`);
return false;
}
mesend(): void {
logger.warning("mesend");
logger.warn("mesend");
}
list(list_items: string[]): void {
logger.warning(`list([${list_items.map(i => `"${i}"`).join(", ")}])`);
logger.warn(`list([${list_items.map(i => `"${i}"`).join(", ")}])`);
}
warning(msg: string, inst_ptr?: InstructionPointer): void {
logger.warning(msg + this.srcloc_to_string(inst_ptr?.source_location));
logger.warn(msg + this.srcloc_to_string(inst_ptr?.source_location));
}
error(err: Error, inst_ptr?: InstructionPointer): void {

View File

@ -161,7 +161,7 @@ export class QuestEditorStore extends Store {
if (section) {
entity.set_section(section);
} else {
logger.warning(`Section ${entity.section_id.val} not found.`);
logger.warn(`Section ${entity.section_id.val} not found.`);
}
};
}

View File

@ -118,7 +118,7 @@ function build_event_dags(
let data = data_map.get(key);
if (data && data.event) {
logger.warning(`Ignored duplicate event #${event.id} for area ${event.area_id}.`);
logger.warn(`Ignored duplicate event #${event.id} for area ${event.area_id}.`);
continue;
}
@ -176,7 +176,7 @@ function build_event_dags(
}
break;
default:
logger.warning(`Unknown event action type: ${(action as any).type}.`);
logger.warn(`Unknown event action type: ${(action as any).type}.`);
break;
}
}
@ -210,7 +210,7 @@ function build_event_dags(
if (child.event) {
event_dags.get(data.area_id)!.add_edge(data.event, child.event);
} else {
logger.warning(`Event ${data.event.id} calls nonexistent event ${child_id}.`);
logger.warn(`Event ${data.event.id} calls nonexistent event ${child_id}.`);
}
}
}