mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-04 22:58:29 +08:00
Quests can now be saved in GC format.
This commit is contained in:
parent
7c9a74171e
commit
b276ba988e
@ -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);
|
||||
};
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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 [];
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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}.`,
|
||||
);
|
||||
}
|
||||
|
28
src/core/data_formats/parsing/quest/BinFormat.ts
Normal file
28
src/core/data_formats/parsing/quest/BinFormat.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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];
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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.");
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -27,8 +27,7 @@
|
||||
}
|
||||
|
||||
.core_Dialog_body {
|
||||
user-select: text;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
|
@ -134,6 +134,7 @@ export class Menu<T> extends Widget {
|
||||
|
||||
case "Enter":
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.select_hovered();
|
||||
break;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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}).`,
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 => {
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
|
||||
|
@ -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.");
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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.`);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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}.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user