diff --git a/src/data_formats/cursor/ArrayBufferCursor.ts b/src/data_formats/cursor/ArrayBufferCursor.ts index d75d85de..91b3ad6d 100644 --- a/src/data_formats/cursor/ArrayBufferCursor.ts +++ b/src/data_formats/cursor/ArrayBufferCursor.ts @@ -49,9 +49,8 @@ export class ArrayBufferCursor implements Cursor { protected buffer: ArrayBuffer; protected dv: DataView; - - private utf16_decoder: TextDecoder = UTF_16BE_DECODER; - private utf16_encoder: TextEncoder = UTF_16BE_ENCODER; + protected utf16_decoder!: TextDecoder; + protected utf16_encoder!: TextEncoder; /** * @param buffer The buffer to read from. diff --git a/src/data_formats/cursor/ResizableBufferCursor.ts b/src/data_formats/cursor/ResizableBufferCursor.ts index 8283e2a8..8184c441 100644 --- a/src/data_formats/cursor/ResizableBufferCursor.ts +++ b/src/data_formats/cursor/ResizableBufferCursor.ts @@ -51,8 +51,8 @@ export class ResizableBufferCursor implements Cursor { return this.buffer.view; } - private utf16_decoder: TextDecoder = UTF_16BE_DECODER; - private utf16_encoder: TextEncoder = UTF_16BE_ENCODER; + protected utf16_decoder: TextDecoder = UTF_16BE_DECODER; + protected utf16_encoder: TextEncoder = UTF_16BE_ENCODER; /** * @param buffer The buffer to read from. diff --git a/src/data_formats/cursor/WritableArrayBufferCursor.ts b/src/data_formats/cursor/WritableArrayBufferCursor.ts index 0507a922..a2c10c2b 100644 --- a/src/data_formats/cursor/WritableArrayBufferCursor.ts +++ b/src/data_formats/cursor/WritableArrayBufferCursor.ts @@ -1,7 +1,8 @@ -import { ArrayBufferCursor } from "./ArrayBufferCursor"; -import { WritableCursor } from "./WritableCursor"; import { ASCII_ENCODER } from "."; +import { Vec2, Vec3 } from "../vector"; +import { ArrayBufferCursor } from "./ArrayBufferCursor"; import { Cursor } from "./Cursor"; +import { WritableCursor } from "./WritableCursor"; /** * A cursor for reading and writing from an array buffer or part of an array buffer. @@ -42,6 +43,21 @@ export class WritableArrayBufferCursor extends ArrayBufferCursor implements Writ return this; } + write_vec2_f32(value: Vec2): this { + this.dv.setFloat32(this.position, value.x, this.little_endian); + this.dv.setFloat32(this.position + 4, value.y, this.little_endian); + this._position += 8; + return this; + } + + write_vec3_f32(value: Vec3): this { + this.dv.setFloat32(this.position, value.x, this.little_endian); + this.dv.setFloat32(this.position + 4, value.y, this.little_endian); + this.dv.setFloat32(this.position + 8, value.z, this.little_endian); + this._position += 12; + return this; + } + write_cursor(other: Cursor): this { const size = other.size - other.position; other.copy_to_uint8_array( @@ -53,18 +69,32 @@ export class WritableArrayBufferCursor extends ArrayBufferCursor implements Writ } write_string_ascii(str: string, byte_length: number): this { + const encoded = ASCII_ENCODER.encode(str); + const encoded_length = Math.min(encoded.byteLength, byte_length); let i = 0; - for (const byte of ASCII_ENCODER.encode(str)) { - if (i < byte_length) { - this.write_u8(byte); - ++i; - } + while (i < encoded_length) { + this.write_u8(encoded[i++]); } - while (i < byte_length) { + while (i++ < byte_length) { + this.write_u8(0); + } + + return this; + } + + write_string_utf16(str: string, byte_length: number): this { + const encoded = this.utf16_encoder.encode(str); + const encoded_length = Math.min(encoded.byteLength, byte_length); + let i = 0; + + while (i < encoded_length) { + this.write_u8(encoded[i++]); + } + + while (i++ < byte_length) { this.write_u8(0); - ++i; } return this; diff --git a/src/data_formats/cursor/WritableCursor.ts b/src/data_formats/cursor/WritableCursor.ts index 94598c78..9f84ffd2 100644 --- a/src/data_formats/cursor/WritableCursor.ts +++ b/src/data_formats/cursor/WritableCursor.ts @@ -1,4 +1,5 @@ import { Cursor } from "./Cursor"; +import { Vec2, Vec3 } from "../vector"; /** * A cursor for reading and writing binary data. @@ -36,6 +37,16 @@ export interface WritableCursor extends Cursor { */ write_u8_array(array: number[]): this; + /** + * Writes two 32-bit floating point numbers and increments position by 8. + */ + write_vec2_f32(value: Vec2): this; + + /** + * Writes three 32-bit floating point numbers and increments position by 12. + */ + write_vec3_f32(value: Vec3): this; + /** * Writes the contents of the given cursor from its position to its end. Increments this cursor's and the given cursor's position by the size of the given cursor. */ @@ -45,4 +56,9 @@ export interface WritableCursor extends Cursor { * Writes byte_length characters of str. If str is shorter than byte_length, nul bytes will be inserted until byte_length bytes have been written. */ write_string_ascii(str: string, byte_length: number): this; + + /** + * Writes characters of str without writing more than byte_length bytes. If less than byte_length bytes can be written this way, nul bytes will be inserted until byte_length bytes have been written. + */ + write_string_utf16(str: string, byte_length: number): this; } diff --git a/src/data_formats/cursor/WritableResizableBufferCursor.ts b/src/data_formats/cursor/WritableResizableBufferCursor.ts index 4ede1ad0..ca637f67 100644 --- a/src/data_formats/cursor/WritableResizableBufferCursor.ts +++ b/src/data_formats/cursor/WritableResizableBufferCursor.ts @@ -2,6 +2,7 @@ import { WritableCursor } from "./WritableCursor"; import { ResizableBufferCursor } from "./ResizableBufferCursor"; import { Cursor } from "./Cursor"; import { ASCII_ENCODER } from "."; +import { Vec3, Vec2 } from "../vector"; export class WritableResizableBufferCursor extends ResizableBufferCursor implements WritableCursor { get size(): number { @@ -59,6 +60,23 @@ export class WritableResizableBufferCursor extends ResizableBufferCursor impleme return this; } + write_vec2_f32(value: Vec2): this { + this.ensure_size(8); + this.dv.setFloat32(this.position, value.x, this.little_endian); + this.dv.setFloat32(this.position + 4, value.y, this.little_endian); + this._position += 8; + return this; + } + + write_vec3_f32(value: Vec3): this { + this.ensure_size(12); + this.dv.setFloat32(this.position, value.x, this.little_endian); + this.dv.setFloat32(this.position + 4, value.y, this.little_endian); + this.dv.setFloat32(this.position + 8, value.z, this.little_endian); + this._position += 12; + return this; + } + write_cursor(other: Cursor): this { const size = other.size - other.position; this.ensure_size(size); @@ -75,18 +93,34 @@ export class WritableResizableBufferCursor extends ResizableBufferCursor impleme write_string_ascii(str: string, byte_length: number): this { this.ensure_size(byte_length); + const encoded = ASCII_ENCODER.encode(str); + const encoded_length = Math.min(encoded.byteLength, byte_length); let i = 0; - for (const byte of ASCII_ENCODER.encode(str)) { - if (i < byte_length) { - this.write_u8(byte); - ++i; - } + while (i < encoded_length) { + this.write_u8(encoded[i++]); } - while (i < byte_length) { + while (i++ < byte_length) { + this.write_u8(0); + } + + return this; + } + + write_string_utf16(str: string, byte_length: number): this { + this.ensure_size(byte_length); + + const encoded = this.utf16_encoder.encode(str); + const encoded_length = Math.min(encoded.byteLength, byte_length); + let i = 0; + + while (i < encoded_length) { + this.write_u8(encoded[i++]); + } + + while (i++ < byte_length) { this.write_u8(0); - ++i; } return this; diff --git a/src/data_formats/cursor/index.ts b/src/data_formats/cursor/index.ts index 6d4ec7f5..0b2a682d 100644 --- a/src/data_formats/cursor/index.ts +++ b/src/data_formats/cursor/index.ts @@ -6,5 +6,9 @@ export const UTF_16BE_DECODER = new TextDecoder("utf-16be"); export const UTF_16LE_DECODER = new TextDecoder("utf-16le"); export const ASCII_ENCODER = new TextEncoder("ascii"); -export const UTF_16BE_ENCODER = new TextEncoder("utf-16be"); -export const UTF_16LE_ENCODER = new TextEncoder("utf-16le"); +export const UTF_16BE_ENCODER = new TextEncoder("utf-16be", { + NONSTANDARD_allowLegacyEncoding: true, +}); +export const UTF_16LE_ENCODER = new TextEncoder("utf-16le", { + NONSTANDARD_allowLegacyEncoding: true, +}); diff --git a/src/data_formats/parsing/quest/bin.test.ts b/src/data_formats/parsing/quest/bin.test.ts index 4ae0ee94..440fcd19 100644 --- a/src/data_formats/parsing/quest/bin.test.ts +++ b/src/data_formats/parsing/quest/bin.test.ts @@ -1,29 +1,35 @@ -import * as fs from "fs"; -import * as prs from "../../compression/prs"; -import { parse_bin, write_bin } from "./bin"; +import { readFileSync } from "fs"; import { Endianness } from "../.."; -import { BufferCursor } from "../../cursor/BufferCursor"; +import * as prs from "../../compression/prs"; import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; +import { BufferCursor } from "../../cursor/BufferCursor"; +import { parse_bin, write_bin } from "./bin"; /** * Parse a file, convert the resulting structure to BIN again and check whether the end result is equal to the original. */ test("parse_bin and write_bin", () => { - const orig_buffer = fs.readFileSync("test/resources/quest118_e.bin"); + const orig_buffer = readFileSync("test/resources/quest118_e.bin"); const orig_bin = prs.decompress(new BufferCursor(orig_buffer, Endianness.Little)); const test_bin = new ArrayBufferCursor(write_bin(parse_bin(orig_bin)), Endianness.Little); - orig_bin.seek_start(0); + orig_bin.seek_start(0); expect(test_bin.size).toBe(orig_bin.size); - let match = true; + let matching_bytes = 0; while (orig_bin.bytes_left) { - if (test_bin.u8() !== orig_bin.u8()) { - match = false; - break; + const test_byte = test_bin.u8(); + const orig_byte = orig_bin.u8(); + + if (test_byte !== orig_byte) { + throw new Error( + `Byte ${matching_bytes} didn't match, expected ${orig_byte}, got ${test_byte}.` + ); } + + matching_bytes++; } - expect(match).toBe(true); + expect(matching_bytes).toBe(orig_bin.size); }); diff --git a/src/data_formats/parsing/quest/bin.ts b/src/data_formats/parsing/quest/bin.ts index 7a4eef37..6789fd07 100644 --- a/src/data_formats/parsing/quest/bin.ts +++ b/src/data_formats/parsing/quest/bin.ts @@ -1,9 +1,12 @@ import Logger from "js-logger"; import { Cursor } from "../../cursor/Cursor"; +import { WritableArrayBufferCursor } from "../../cursor/WritableArrayBufferCursor"; +import { Endianness } from "../.."; +import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor"; const logger = Logger.get("data_formats/parsing/quest/bin"); -export interface BinFile { +export type BinFile = { quest_id: number; language: number; quest_name: string; @@ -11,11 +14,12 @@ export interface BinFile { long_description: string; function_offsets: number[]; instructions: Instruction[]; - data: ArrayBuffer; -} + object_code: ArrayBuffer; + unknown: ArrayBuffer; +}; export function parse_bin(cursor: Cursor, lenient: boolean = false): BinFile { - const object_code_offset = cursor.u32(); + const object_code_offset = cursor.u32(); // Always 4652 const function_offset_table_offset = cursor.u32(); // Relative offsets const size = cursor.u32(); cursor.seek(4); // Always seems to be 0xFFFFFFFF @@ -29,6 +33,14 @@ export function parse_bin(cursor: Cursor, lenient: boolean = false): BinFile { logger.warn(`Value ${size} in bin size field does not match actual size ${cursor.size}.`); } + const unknown = cursor.take(object_code_offset - cursor.position).array_buffer(); + + const object_code = cursor + .seek_start(object_code_offset) + .take(function_offset_table_offset - object_code_offset); + + const instructions = parse_object_code(object_code, lenient); + const function_offset_count = Math.floor((cursor.size - function_offset_table_offset) / 4); cursor.seek_start(function_offset_table_offset); @@ -38,13 +50,6 @@ export function parse_bin(cursor: Cursor, lenient: boolean = false): BinFile { function_offsets.push(cursor.i32()); } - const instructions = parse_object_code( - cursor - .seek_start(object_code_offset) - .take(function_offset_table_offset - object_code_offset), - lenient - ); - return { quest_id, language, @@ -53,20 +58,49 @@ export function parse_bin(cursor: Cursor, lenient: boolean = false): BinFile { long_description, function_offsets, instructions, - data: cursor.seek_start(0).array_buffer(), + object_code: object_code.seek_start(0).array_buffer(), + unknown, }; } -export function write_bin({ data }: { data: ArrayBuffer }): ArrayBuffer { - return data; +export function write_bin(bin: BinFile): ArrayBuffer { + const object_code_offset = 4652; + const buffer = new ArrayBuffer( + object_code_offset + bin.object_code.byteLength + 4 * bin.function_offsets.length + ); + const cursor = new WritableArrayBufferCursor(buffer, Endianness.Little); + + cursor.write_u32(object_code_offset); + cursor.write_u32(object_code_offset + bin.object_code.byteLength); + cursor.write_u32(buffer.byteLength); + 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_cursor(new ArrayBufferCursor(bin.unknown, Endianness.Little)); + + while (cursor.position < object_code_offset) { + cursor.write_u8(0); + } + + cursor.write_cursor(new ArrayBufferCursor(bin.object_code, Endianness.Little)); + + for (const function_offset of bin.function_offsets) { + cursor.write_i32(function_offset); + } + + return buffer; } -export interface Instruction { +export type Instruction = { opcode: number; mnemonic: string; args: any[]; size: number; -} +}; function parse_object_code(cursor: Cursor, lenient: boolean): Instruction[] { const instructions = []; diff --git a/src/data_formats/parsing/quest/dat.ts b/src/data_formats/parsing/quest/dat.ts index 18f7906e..76f91ba1 100644 --- a/src/data_formats/parsing/quest/dat.ts +++ b/src/data_formats/parsing/quest/dat.ts @@ -22,6 +22,7 @@ export type DatEntity = { section_id: number; position: Vec3; rotation: Vec3; + scale: Vec3; area_id: number; unknown: number[][]; }; @@ -29,7 +30,6 @@ export type DatEntity = { export type DatObject = DatEntity; export type DatNpc = DatEntity & { - flags: number; skin: number; }; @@ -72,20 +72,19 @@ export function parse_dat(cursor: Cursor): DatFile { const unknown1 = cursor.u8_array(10); const section_id = cursor.u16(); const unknown2 = cursor.u8_array(2); - const x = cursor.f32(); - const y = cursor.f32(); - const z = cursor.f32(); + const position = cursor.vec3_f32(); const rotation_x = (cursor.i32() / 0xffff) * 2 * Math.PI; const rotation_y = (cursor.i32() / 0xffff) * 2 * Math.PI; const rotation_z = (cursor.i32() / 0xffff) * 2 * Math.PI; - // The next 3 floats seem to be scale values. - const unknown3 = cursor.u8_array(28); + const scale = cursor.vec3_f32(); + const unknown3 = cursor.u8_array(16); objs.push({ type_id, section_id, - position: new Vec3(x, y, z), + position, rotation: new Vec3(rotation_x, rotation_y, rotation_z), + scale, area_id, unknown: [unknown1, unknown2, unknown3], }); @@ -109,27 +108,24 @@ export function parse_dat(cursor: Cursor): DatFile { const unknown1 = cursor.u8_array(10); const section_id = cursor.u16(); const unknown2 = cursor.u8_array(6); - const x = cursor.f32(); - const y = cursor.f32(); - const z = cursor.f32(); + const position = cursor.vec3_f32(); const rotation_x = (cursor.i32() / 0xffff) * 2 * Math.PI; const rotation_y = (cursor.i32() / 0xffff) * 2 * Math.PI; const rotation_z = (cursor.i32() / 0xffff) * 2 * Math.PI; - const unknown3 = cursor.u8_array(4); - const flags = cursor.f32(); - const unknown4 = cursor.u8_array(12); + const scale = cursor.vec3_f32(); + const unknown3 = cursor.u8_array(8); const skin = cursor.u32(); - const unknown5 = cursor.u8_array(4); + const unknown4 = cursor.u8_array(4); npcs.push({ type_id, section_id, - position: new Vec3(x, y, z), + position, rotation: new Vec3(rotation_x, rotation_y, rotation_z), + scale, skin, area_id, - flags, - unknown: [unknown1, unknown2, unknown3, unknown4, unknown5], + unknown: [unknown1, unknown2, unknown3, unknown4], }); } @@ -179,16 +175,30 @@ export function write_dat({ objs, npcs, unknowns }: DatFile): ResizableBuffer { cursor.write_u32(entities_size); for (const obj of area_objs) { + if (obj.unknown.length !== 3) + throw new Error(`unknown should be of length 3, was ${obj.unknown.length}`); + cursor.write_u16(obj.type_id); + + if (obj.unknown[0].length !== 10) + throw new Error(`unknown[0] should be of length 10, was ${obj.unknown[0].length}`); + cursor.write_u8_array(obj.unknown[0]); cursor.write_u16(obj.section_id); + + if (obj.unknown[1].length !== 2) + throw new Error(`unknown[1] should be of length 2, was ${obj.unknown[1].length}`); + cursor.write_u8_array(obj.unknown[1]); - cursor.write_f32(obj.position.x); - cursor.write_f32(obj.position.y); - cursor.write_f32(obj.position.z); + cursor.write_vec3_f32(obj.position); cursor.write_i32(Math.round((obj.rotation.x / (2 * Math.PI)) * 0xffff)); cursor.write_i32(Math.round((obj.rotation.y / (2 * Math.PI)) * 0xffff)); cursor.write_i32(Math.round((obj.rotation.z / (2 * Math.PI)) * 0xffff)); + cursor.write_vec3_f32(obj.scale); + + if (obj.unknown[2].length !== 16) + throw new Error(`unknown[2] should be of length 16, was ${obj.unknown[2].length}`); + cursor.write_u8_array(obj.unknown[2]); } } @@ -211,17 +221,14 @@ export function write_dat({ objs, npcs, unknowns }: DatFile): ResizableBuffer { cursor.write_u8_array(npc.unknown[0]); cursor.write_u16(npc.section_id); cursor.write_u8_array(npc.unknown[1]); - cursor.write_f32(npc.position.x); - cursor.write_f32(npc.position.y); - cursor.write_f32(npc.position.z); + cursor.write_vec3_f32(npc.position); cursor.write_i32(Math.round((npc.rotation.x / (2 * Math.PI)) * 0xffff)); cursor.write_i32(Math.round((npc.rotation.y / (2 * Math.PI)) * 0xffff)); cursor.write_i32(Math.round((npc.rotation.z / (2 * Math.PI)) * 0xffff)); + cursor.write_vec3_f32(npc.scale); cursor.write_u8_array(npc.unknown[2]); - cursor.write_f32(npc.flags); - cursor.write_u8_array(npc.unknown[3]); cursor.write_u32(npc.skin); - cursor.write_u8_array(npc.unknown[4]); + cursor.write_u8_array(npc.unknown[3]); } } diff --git a/src/data_formats/parsing/quest/index.test.ts b/src/data_formats/parsing/quest/index.test.ts index fba951d1..19f7c1f6 100644 --- a/src/data_formats/parsing/quest/index.test.ts +++ b/src/data_formats/parsing/quest/index.test.ts @@ -41,8 +41,7 @@ test("parse Towards the Future", () => { */ test("parse_quest and write_quest_qst", () => { const buffer = fs.readFileSync("test/resources/tethealla_v0.143_quests/solo/ep1/02.qst"); - const cursor = new BufferCursor(buffer, Endianness.Little); - const orig_quest = parse_quest(cursor)!; + const orig_quest = parse_quest(new BufferCursor(buffer, Endianness.Little))!; const test_quest = parse_quest( new ArrayBufferCursor(write_quest_qst(orig_quest, "02.qst"), Endianness.Little) )!; diff --git a/src/data_formats/parsing/quest/index.ts b/src/data_formats/parsing/quest/index.ts index dda756a5..140d97f0 100644 --- a/src/data_formats/parsing/quest/index.ts +++ b/src/data_formats/parsing/quest/index.ts @@ -72,7 +72,8 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u } return new Quest( - dat_file.id, + bin.quest_id, + bin.language, bin.quest_name, bin.short_description, bin.long_description, @@ -81,19 +82,32 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u parse_obj_data(dat.objs), parse_npc_data(episode, dat.npcs), dat.unknowns, - bin.data + bin.function_offsets, + bin.object_code, + bin.unknown ); } export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer { const dat = write_dat({ objs: objects_to_dat_data(quest.objects), - npcs: npcsToDatData(quest.npcs), + npcs: npcs_to_dat_data(quest.npcs), unknowns: quest.dat_unknowns, }); - const bin = write_bin({ data: quest.bin_data }); + 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, + function_offsets: quest.function_offsets, + instructions: [], + object_code: quest.object_code, + unknown: quest.bin_unknown, + }); const ext_start = file_name.lastIndexOf("."); - const base_file_name = ext_start === -1 ? file_name : file_name.slice(0, ext_start); + const base_file_name = + ext_start === -1 ? file_name.slice(0, 12) : file_name.slice(0, Math.min(12, ext_start)); return write_qst({ files: [ @@ -208,37 +222,37 @@ function get_func_operations( function parse_obj_data(objs: DatObject[]): QuestObject[] { return objs.map(obj_data => { - const { x, y, z } = obj_data.position; - const rot = obj_data.rotation; return new QuestObject( + ObjectType.from_pso_id(obj_data.type_id), obj_data.area_id, obj_data.section_id, - new Vec3(x, y, z), - new Vec3(rot.x, rot.y, rot.z), - ObjectType.from_pso_id(obj_data.type_id), - obj_data + obj_data.position.clone(), + obj_data.rotation.clone(), + obj_data.scale.clone(), + obj_data.unknown ); }); } function parse_npc_data(episode: number, npcs: DatNpc[]): QuestNpc[] { return npcs.map(npc_data => { - const { x, y, z } = npc_data.position; - const rot = npc_data.rotation; return new QuestNpc( + get_npc_type(episode, npc_data), + npc_data.type_id, + npc_data.skin, npc_data.area_id, npc_data.section_id, - new Vec3(x, y, z), - new Vec3(rot.x, rot.y, rot.z), - get_npc_type(episode, npc_data), - npc_data + npc_data.position.clone(), + npc_data.rotation.clone(), + npc_data.scale.clone(), + npc_data.unknown ); }); } // TODO: detect Mothmant, St. Rappy, Hallo Rappy, Egg Rappy, Death Gunner, Bulk and Recon. -function get_npc_type(episode: number, { type_id, flags, skin, area_id }: DatNpc): NpcType { - const regular = Math.abs(flags - 1) > 0.00001; +function get_npc_type(episode: number, { type_id, scale, skin, area_id }: DatNpc): NpcType { + const regular = Math.abs(scale.y - 1) > 0.00001; switch (`${type_id}, ${skin % 3}, ${episode}`) { case `${0x044}, 0, 1`: @@ -516,32 +530,37 @@ function objects_to_dat_data(objects: QuestObject[]): DatObject[] { return objects.map(object => ({ type_id: object.type.pso_id!, section_id: object.section_id, - position: object.section_position, - rotation: object.rotation, + position: object.section_position.clone(), + rotation: object.rotation.clone(), + scale: object.scale.clone(), area_id: object.area_id, - unknown: object.dat.unknown, + unknown: object.unknown, })); } -function npcsToDatData(npcs: QuestNpc[]): DatNpc[] { +function npcs_to_dat_data(npcs: QuestNpc[]): DatNpc[] { return npcs.map(npc => { - // If the type is unknown, typeData will be undefined and we use the raw data from the DAT file. - const type_data = npc_type_to_dat_data(npc.type); - let flags = npc.dat.flags; + const type_data = npc_type_to_dat_data(npc.type) || { + type_id: npc.pso_type_id, + skin: npc.pso_skin, + regular: true, + }; - if (type_data) { - flags = (npc.dat.flags & ~0x800000) | (type_data.regular ? 0 : 0x800000); - } + let scale = new Vec3( + npc.scale.x, + (npc.scale.y & ~0x800000) | (type_data.regular ? 0 : 0x800000), + npc.scale.z + ); return { - type_id: type_data ? type_data.type_id : npc.dat.type_id, + type_id: type_data.type_id, section_id: npc.section_id, - position: npc.section_position, - rotation: npc.rotation, - flags, - skin: type_data ? type_data.skin : npc.dat.skin, + position: npc.section_position.clone(), + rotation: npc.rotation.clone(), + scale, + skin: type_data.skin, area_id: npc.area_id, - unknown: npc.dat.unknown, + unknown: npc.unknown, }; }); } diff --git a/src/domain/index.ts b/src/domain/index.ts index 06ba1792..5070dbc6 100644 --- a/src/domain/index.ts +++ b/src/domain/index.ts @@ -79,7 +79,8 @@ export class Section { } export class Quest { - @observable id?: number; + @observable id: number; + @observable language: number; @observable name: string; @observable short_description: string; @observable long_description: string; @@ -91,13 +92,13 @@ export class Quest { * (Partial) raw DAT data that can't be parsed yet by Phantasmal. */ dat_unknowns: DatUnknown[]; - /** - * (Partial) raw BIN data that can't be parsed yet by Phantasmal. - */ - bin_data: ArrayBuffer; + function_offsets: number[]; + object_code: ArrayBuffer; + bin_unknown: ArrayBuffer; constructor( - id: number | undefined, + id: number, + language: number, name: string, short_description: string, long_description: string, @@ -106,15 +107,24 @@ export class Quest { objects: QuestObject[], npcs: QuestNpc[], dat_unknowns: DatUnknown[], - bin_data: ArrayBuffer + function_offsets: number[], + object_code: ArrayBuffer, + bin_unknown: ArrayBuffer ) { - if (id != null && (!Number.isInteger(id) || id < 0)) - throw new Error("id should be undefined or a non-negative integer."); + if (!Number.isInteger(id) || id < 0) + throw new Error("id should be a non-negative integer."); + if (!Number.isInteger(language)) throw new Error("language should be an integer."); check_episode(episode); + if (!area_variants) throw new Error("area_variants is required."); if (!objects || !(objects instanceof Array)) throw new Error("objs is required."); if (!npcs || !(npcs instanceof Array)) throw new Error("npcs is required."); + if (!dat_unknowns) throw new Error("dat_unknowns is required."); + if (!function_offsets) throw new Error("function_offsets is required."); + if (!object_code) throw new Error("object_code is required."); + if (!bin_unknown) throw new Error("bin_unknown is required."); this.id = id; + this.language = language; this.name = name; this.short_description = short_description; this.long_description = long_description; @@ -123,7 +133,9 @@ export class Quest { this.objects = objects; this.npcs = npcs; this.dat_unknowns = dat_unknowns; - this.bin_data = bin_data; + this.function_offsets = function_offsets; + this.object_code = object_code; + this.bin_unknown = bin_unknown; } } @@ -156,6 +168,8 @@ export class QuestEntity { @observable.ref rotation: Vec3; + @observable.ref scale: Vec3; + /** * Section-relative position */ @@ -193,7 +207,14 @@ export class QuestEntity { } } - constructor(type: Type, area_id: number, section_id: number, position: Vec3, rotation: Vec3) { + constructor( + type: Type, + area_id: number, + section_id: number, + position: Vec3, + rotation: Vec3, + scale: Vec3 + ) { if (Object.getPrototypeOf(this) === Object.getPrototypeOf(QuestEntity)) throw new Error("Abstract class should not be instantiated directly."); if (!type) throw new Error("type is required."); @@ -203,12 +224,14 @@ export class QuestEntity { throw new Error(`Expected section_id to be a non-negative integer, got ${section_id}.`); if (!position) throw new Error("position is required."); if (!rotation) throw new Error("rotation is required."); + if (!scale) throw new Error("scale is required."); this.type = type; this.area_id = area_id; this._section_id = section_id; this.position = position; this.rotation = rotation; + this.scale = scale; } @action @@ -221,46 +244,52 @@ export class QuestEntity { export class QuestObject extends QuestEntity { @observable type: ObjectType; /** - * The raw data from a DAT file. + * Data of which the purpose hasn't been discovered yet. */ - dat: DatObject; + unknown: number[][]; constructor( + type: ObjectType, area_id: number, section_id: number, position: Vec3, rotation: Vec3, - type: ObjectType, - dat: DatObject + scale: Vec3, + unknown: number[][] ) { - super(type, area_id, section_id, position, rotation); + super(type, area_id, section_id, position, rotation, scale); this.type = type; - this.dat = dat; + this.unknown = unknown; } } export class QuestNpc extends QuestEntity { @observable type: NpcType; + pso_type_id: number; + pso_skin: number; /** - * The raw data from a DAT file. + * Data of which the purpose hasn't been discovered yet. */ - dat: DatNpc; + unknown: number[][]; constructor( + type: NpcType, + pso_type_id: number, + pso_skin: number, area_id: number, section_id: number, position: Vec3, rotation: Vec3, - type: NpcType, - dat: DatNpc + scale: Vec3, + unknown: number[][] ) { - super(type, area_id, section_id, position, rotation); - - if (!type) throw new Error("type is required."); + super(type, area_id, section_id, position, rotation, scale); this.type = type; - this.dat = dat; + this.pso_type_id = pso_type_id; + this.pso_skin = pso_skin; + this.unknown = unknown; } } diff --git a/src/stores/QuestEditorStore.ts b/src/stores/QuestEditorStore.ts index bffa074d..a0db7b39 100644 --- a/src/stores/QuestEditorStore.ts +++ b/src/stores/QuestEditorStore.ts @@ -1,34 +1,27 @@ import Logger from "js-logger"; -import { action, observable, runInAction } from "mobx"; +import { action, flow, observable } from "mobx"; import { Endianness } from "../data_formats"; import { ArrayBufferCursor } from "../data_formats/cursor/ArrayBufferCursor"; import { parse_quest, write_quest_qst } from "../data_formats/parsing/quest"; import { Vec3 } from "../data_formats/vector"; -import { Area, Quest, QuestEntity, Section } from "../domain"; +import { Area, Episode, Quest, QuestEntity, Section } from "../domain"; import { read_file } from "../read_file"; -import { area_store } from "./AreaStore"; import { UndoStack } from "../undo"; +import { area_store } from "./AreaStore"; +import { create_new_quest } from "./quest_creation"; const logger = Logger.get("stores/QuestEditorStore"); class QuestEditorStore { readonly undo_stack = new UndoStack(); + + @observable current_quest_filename?: string; @observable current_quest?: Quest; @observable current_area?: Area; @observable selected_entity?: QuestEntity; - @action - set_quest = (quest?: Quest) => { - this.undo_stack.clear(); - this.selected_entity = undefined; - this.current_quest = quest; - - if (quest && quest.area_variants.length) { - this.current_area = quest.area_variants[0].area; - } else { - this.current_area = undefined; - } - }; + @observable save_dialog_filename?: string; + @observable save_dialog_open: boolean = false; @action set_selected_entity = (entity?: QuestEntity) => { @@ -53,17 +46,81 @@ class QuestEditorStore { } }; + @action + new_quest = (episode: Episode) => { + this.set_quest(create_new_quest(episode)); + }; + // TODO: notify user of problems. - load_file = async (file: File) => { + open_file = flow(function* open_file(this: QuestEditorStore, filename: string, file: File) { try { - const buffer = await read_file(file); + const buffer = yield read_file(file); const quest = parse_quest(new ArrayBufferCursor(buffer, Endianness.Little)); + this.current_quest_filename = filename; this.set_quest(quest); + } catch (e) { + logger.error("Couldn't read file.", e); + } + }); + + @action + open_save_dialog = () => { + this.save_dialog_filename = this.current_quest_filename + ? this.current_quest_filename.endsWith(".qst") + ? this.current_quest_filename.slice(0, -4) + : this.current_quest_filename + : ""; + + this.save_dialog_open = true; + }; + + @action + close_save_dialog = () => { + this.save_dialog_open = false; + }; + + @action + set_save_dialog_filename = (filename: string) => { + this.save_dialog_filename = filename; + }; + + save_current_quest_to_file = (file_name: string) => { + if (this.current_quest) { + const buffer = write_quest_qst(this.current_quest, file_name); + + if (!file_name.endsWith(".qst")) { + file_name += ".qst"; + } + + const a = document.createElement("a"); + a.href = URL.createObjectURL(new Blob([buffer], { type: "application/octet-stream" })); + a.download = file_name; + document.body.appendChild(a); + a.click(); + URL.revokeObjectURL(a.href); + document.body.removeChild(a); + } + + this.save_dialog_open = false; + }; + + @action + private set_quest = flow(function* set_quest(this: QuestEditorStore, quest?: Quest) { + if (quest !== this.current_quest) { + this.undo_stack.clear(); + this.selected_entity = undefined; + this.current_quest = quest; + + if (quest && quest.area_variants.length) { + this.current_area = quest.area_variants[0].area; + } else { + this.current_area = undefined; + } if (quest) { // Load section data. for (const variant of quest.area_variants) { - const sections = await area_store.get_area_sections( + const sections = yield area_store.get_area_sections( quest.episode, variant.area.id, variant.id @@ -89,15 +146,10 @@ class QuestEditorStore { } else { logger.error("Couldn't parse quest file."); } - } catch (e) { - logger.error("Couldn't read file.", e); } - }; + }); - private set_section_on_visible_quest_entity = async ( - entity: QuestEntity, - sections: Section[] - ) => { + private set_section_on_visible_quest_entity = (entity: QuestEntity, sections: Section[]) => { let { x, y, z } = entity.position; const section = sections.find(s => s.id === entity.section_id); @@ -113,28 +165,7 @@ class QuestEditorStore { logger.warn(`Section ${entity.section_id} not found.`); } - runInAction(() => { - entity.section = section; - entity.position = new Vec3(x, y, z); - }); - }; - - save_current_quest_to_file = (file_name: string) => { - if (this.current_quest) { - const buffer = write_quest_qst(this.current_quest, file_name); - - if (!file_name.endsWith(".qst")) { - file_name += ".qst"; - } - - const a = document.createElement("a"); - a.href = URL.createObjectURL(new Blob([buffer], { type: "application/octet-stream" })); - a.download = file_name; - document.body.appendChild(a); - a.click(); - URL.revokeObjectURL(a.href); - document.body.removeChild(a); - } + entity.set_position_and_section(new Vec3(x, y, z), section); }; } diff --git a/src/stores/quest_creation.ts b/src/stores/quest_creation.ts new file mode 100644 index 00000000..8f02a445 --- /dev/null +++ b/src/stores/quest_creation.ts @@ -0,0 +1,643 @@ +import { Quest, ObjectType, QuestObject, Episode, QuestNpc, NpcType } from "../domain"; +import { area_store } from "./AreaStore"; +import { Vec3 } from "../data_formats/vector"; + +export function create_new_quest(episode: Episode): Quest { + if (episode === Episode.II) throw new Error("Episode II not yet supported."); + + return new Quest( + 0, + 0, + "Untitled", + "Created with phantasmal.world.", + "Created with phantasmal.world.", + episode, + [area_store.get_variant(episode, 0, 0)], + create_default_objects(), + create_default_npcs(), + [], + [], + new ArrayBuffer(0), + new ArrayBuffer(0) + ); +} + +function create_default_objects(): QuestObject[] { + return [ + new QuestObject( + ObjectType.MenuActivation, + 0, + 10, + new Vec3(-16.313568115234375, 3, -579.5118408203125), + new Vec3(0.0009587526218325454, 0, 0), + new Vec3(1, 1, 1), + [ + [2, 0, 0, 0, 0, 0, 0, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 75, 251, 140], + ] + ), + new QuestObject( + ObjectType.MenuActivation, + 0, + 10, + new Vec3(-393.07318115234375, 10, -12.964752197265625), + new Vec3(0, 0, 0), + new Vec3(9.183549615799121e-41, 1.0000011920928955, 1), + [ + [2, 0, 1, 0, 0, 0, 1, 64, 0, 0], + [0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 76, 251, 140], + ] + ), + new QuestObject( + ObjectType.MenuActivation, + 0, + 10, + new Vec3(-458.60699462890625, 10, -51.270660400390625), + new Vec3(0, 0, 0), + new Vec3(1, 1, 1), + [ + [2, 0, 2, 0, 0, 0, 2, 64, 0, 0], + [0, 0], + [2, 0, 0, 0, 0, 0, 1, 0, 10, 0, 0, 0, 192, 76, 251, 140], + ] + ), + new QuestObject( + ObjectType.MenuActivation, + 0, + 10, + new Vec3(-430.19696044921875, 10, -24.490447998046875), + new Vec3(0, 0, 0), + new Vec3(1, 1, 1), + [ + [2, 0, 3, 0, 0, 0, 3, 64, 0, 0], + [0, 0], + [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 96, 77, 251, 140], + ] + ), + new QuestObject( + ObjectType.PlayerSet, + 0, + 10, + new Vec3(0.995330810546875, 0, -37.0010986328125), + new Vec3(0, 4.712460886831327, 0), + new Vec3(0, 1, 1), + [ + [2, 0, 4, 0, 0, 0, 4, 64, 0, 0], + [0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 78, 251, 140], + ] + ), + new QuestObject( + ObjectType.PlayerSet, + 0, + 10, + new Vec3(3.0009307861328125, 0, -23.99688720703125), + new Vec3(0, 4.859725289544806, 0), + new Vec3(1.000000238418579, 1, 1), + [ + [2, 0, 5, 0, 0, 0, 5, 64, 0, 0], + [0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 160, 78, 251, 140], + ] + ), + new QuestObject( + ObjectType.PlayerSet, + 0, + 10, + new Vec3(2.0015106201171875, 0, -50.00386047363281), + new Vec3(0, 4.565196484117848, 0), + new Vec3(2.000002384185791, 1, 1), + [ + [2, 0, 6, 0, 0, 0, 6, 64, 0, 0], + [0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 79, 251, 140], + ] + ), + new QuestObject( + ObjectType.PlayerSet, + 0, + 10, + new Vec3(4.9973907470703125, 0, -61.99664306640625), + new Vec3(0, 4.368843947166543, 0), + new Vec3(3.0000007152557373, 1, 1), + [ + [2, 0, 7, 0, 0, 0, 7, 64, 0, 0], + [0, 0], + [0, 0, 1, 0, 10, 0, 0, 0, 0, 0, 0, 0, 224, 79, 251, 140], + ] + ), + new QuestObject( + ObjectType.MainRagolTeleporter, + 0, + 10, + new Vec3(132.00314331054688, 1.000000238418579, -265.002197265625), + new Vec3(0, 0.49088134237826325, 0), + new Vec3(1.000000238418579, 1, 1), + [ + [0, 0, 87, 7, 0, 0, 88, 71, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 208, 128, 250, 140], + ] + ), + new QuestObject( + ObjectType.PrincipalWarp, + 0, + 10, + new Vec3(-228, 0, -2020.99951171875), + new Vec3(0, 2.9452880542695796, 0), + new Vec3(-10.000004768371582, 0, -30.000030517578125), + [ + [2, 0, 9, 0, 0, 0, 9, 64, 0, 0], + [0, 0], + [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 176, 81, 251, 140], + ] + ), + new QuestObject( + ObjectType.MenuActivation, + 0, + 10, + new Vec3(-41.000030517578125, 0, 42.37322998046875), + new Vec3(0, 0, 0), + new Vec3(1, 1, 1), + [ + [2, 0, 10, 0, 0, 0, 10, 64, 0, 0], + [1, 0], + [4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 224, 82, 251, 140], + ] + ), + new QuestObject( + ObjectType.MenuActivation, + 0, + 10, + new Vec3(-479.21673583984375, 8.781256675720215, -322.465576171875), + new Vec3(6.28328118244177, 0.0009587526218325454, 0), + new Vec3(1, 1, 1), + [ + [2, 0, 11, 0, 0, 0, 11, 64, 0, 0], + [0, 0], + [5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 83, 251, 140], + ] + ), + new QuestObject( + ObjectType.PrincipalWarp, + 0, + 10, + new Vec3(-228, 0, -351.0015869140625), + new Vec3(0, 0, 0), + new Vec3(10.000006675720215, 0, -1760.0010986328125), + [ + [2, 0, 12, 0, 0, 0, 12, 64, 0, 0], + [0, 0], + [0, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 84, 251, 140], + ] + ), + new QuestObject( + ObjectType.TelepipeLocation, + 0, + 10, + new Vec3(-561.88232421875, 0, -406.8829345703125), + new Vec3(0, 0, 0), + new Vec3(1, 1, 1), + [ + [2, 0, 13, 0, 0, 0, 13, 64, 0, 0], + [0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 85, 251, 140], + ] + ), + new QuestObject( + ObjectType.TelepipeLocation, + 0, + 10, + new Vec3(-547.8557739257812, 0, -444.8822326660156), + new Vec3(0, 0, 0), + new Vec3(1, 1, 1), + [ + [2, 0, 14, 0, 0, 0, 14, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 86, 251, 140], + ] + ), + new QuestObject( + ObjectType.TelepipeLocation, + 0, + 10, + new Vec3(-486.441650390625, 0, -497.4501647949219), + new Vec3(0, 0, 0), + new Vec3(9.183549615799121e-41, 1.0000011920928955, 1), + [ + [2, 0, 15, 0, 0, 0, 15, 64, 0, 0], + [0, 0], + [3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 208, 86, 251, 140], + ] + ), + new QuestObject( + ObjectType.TelepipeLocation, + 0, + 10, + new Vec3(-522.4052734375, 0, -474.1882629394531), + new Vec3(0, 0, 0), + new Vec3(1, 1, 1), + [ + [2, 0, 16, 0, 0, 0, 16, 64, 0, 0], + [0, 0], + [2, 0, 0, 0, 0, 0, 1, 0, 10, 0, 0, 0, 144, 87, 251, 140], + ] + ), + new QuestObject( + ObjectType.MedicalCenterDoor, + 0, + 10, + new Vec3(-34.49853515625, 0, -384.4951171875), + new Vec3(0, 5.497871034636549, 0), + new Vec3(3.0000007152557373, 1, 1), + [ + [2, 0, 17, 0, 0, 0, 17, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 88, 251, 140], + ] + ), + new QuestObject( + ObjectType.ShopDoor, + 0, + 10, + new Vec3(-393.0031433105469, 0, -143.49981689453125), + new Vec3(0, 3.141640591220885, 0), + new Vec3(3.0000007152557373, 1, 1), + [ + [2, 0, 18, 0, 0, 0, 18, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 89, 251, 140], + ] + ), + new QuestObject( + ObjectType.MenuActivation, + 0, + 10, + new Vec3(-355.17462158203125, 0, -43.15193176269531), + new Vec3(0, 0, 0), + new Vec3(1.000000238418579, 1, 1), + [ + [2, 0, 19, 0, 0, 0, 19, 64, 0, 0], + [0, 0], + [6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 80, 90, 251, 140], + ] + ), + new QuestObject( + ObjectType.HuntersGuildDoor, + 0, + 10, + new Vec3(-43.00239562988281, 0, -118.00120544433594), + new Vec3(0, 3.141640591220885, 0), + new Vec3(3.0000007152557373, 1, 1), + [ + [2, 0, 20, 0, 0, 0, 20, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 90, 251, 140], + ] + ), + new QuestObject( + ObjectType.TeleporterDoor, + 0, + 10, + new Vec3(26.000823974609375, 0, -265.99810791015625), + new Vec3(0, 3.141640591220885, 0), + new Vec3(3.0000007152557373, 1, 1), + [ + [2, 0, 21, 0, 0, 0, 21, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 91, 251, 140], + ] + ), + new QuestObject( + ObjectType.PlayerSet, + 0, + 10, + new Vec3(57.81005859375, 0, -268.5472412109375), + new Vec3(0, 4.712460886831327, 0), + new Vec3(0, 1, 1), + [ + [2, 0, 22, 0, 0, 0, 22, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 240, 92, 251, 140], + ] + ), + new QuestObject( + ObjectType.PlayerSet, + 0, + 10, + new Vec3(66.769287109375, 0, -252.3748779296875), + new Vec3(0, 4.712460886831327, 0), + new Vec3(1.000000238418579, 1, 1), + [ + [2, 0, 23, 0, 0, 0, 23, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 144, 93, 251, 140], + ] + ), + new QuestObject( + ObjectType.PlayerSet, + 0, + 10, + new Vec3(67.36819458007812, 0, -284.9297180175781), + new Vec3(0, 4.712460886831327, 0), + new Vec3(2.000000476837158, 1, 1), + [ + [2, 0, 24, 0, 0, 0, 24, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 48, 94, 251, 140], + ] + ), + new QuestObject( + ObjectType.PlayerSet, + 0, + 10, + new Vec3(77.10488891601562, 0, -269.2830505371094), + new Vec3(0, 4.712460886831327, 0), + new Vec3(3.0000007152557373, 1, 1), + [ + [2, 0, 25, 0, 0, 0, 25, 64, 0, 0], + [0, 0], + [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 208, 94, 251, 140], + ] + ), + ]; +} + +function create_default_npcs(): QuestNpc[] { + return [ + new QuestNpc( + NpcType.GuildLady, + 29, + 0, + 0, + 10, + new Vec3(-49.0010986328125, 0, 50.996429443359375), + new Vec3(0, 2.3562304434156633, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 86, 0, 0, 0, 0, 23, 87], + [0, 0, 0, 0, 0, 0], + [18, 192, 124, 68, 0, 128, 84, 68], + [128, 238, 223, 176], + ] + ), + new QuestNpc( + NpcType.MaleFat, + 11, + 0, + 0, + 10, + new Vec3(-2.9971923828125, 0, 63.999267578125), + new Vec3(0, 2.9452880542695796, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 87, 0, 0, 0, 0, 23, 88], + [0, 0, 0, 0, 0, 0], + [6, 0, 202, 66, 4, 0, 155, 67], + [128, 238, 227, 176], + ] + ), + new QuestNpc( + NpcType.FemaleFat, + 4, + 1, + 0, + 20, + new Vec3(167.99769592285156, 0, 83.99686431884766), + new Vec3(0, 3.927050739026106, 0), + new Vec3(24.000009536743164, 0, 0), + [ + [0, 0, 7, 88, 0, 0, 0, 0, 23, 89], + [0, 0, 0, 0, 0, 0], + [18, 0, 126, 68, 6, 0, 200, 66], + [128, 238, 232, 48], + ] + ), + new QuestNpc( + NpcType.MaleDwarf, + 10, + 1, + 0, + 20, + new Vec3(156.0028839111328, 0, -49.99967575073242), + new Vec3(0, 5.497871034636549, 0), + new Vec3(30.000009536743164, 0, 0), + [ + [0, 0, 7, 89, 0, 0, 0, 0, 23, 90], + [0, 0, 0, 0, 0, 0], + [18, 192, 125, 68, 6, 0, 180, 66], + [128, 238, 236, 176], + ] + ), + new QuestNpc( + NpcType.RedSoldier, + 26, + 0, + 0, + 20, + new Vec3(237.9988250732422, 0, -14.0001220703125), + new Vec3(0, 5.497871034636549, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 90, 0, 0, 0, 0, 23, 91], + [0, 0, 0, 0, 0, 0], + [18, 0, 127, 68, 6, 0, 2, 67], + [128, 238, 241, 48], + ] + ), + new QuestNpc( + NpcType.BlueSoldier, + 25, + 0, + 0, + 20, + new Vec3(238.00379943847656, 0, 63.00413513183594), + new Vec3(0, 3.927050739026106, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 91, 0, 0, 0, 0, 23, 92], + [0, 0, 0, 0, 0, 0], + [18, 192, 126, 68, 11, 0, 240, 66], + [128, 238, 245, 176], + ] + ), + new QuestNpc( + NpcType.FemaleMacho, + 5, + 1, + 0, + 20, + new Vec3(-2.001882553100586, 0, 35.0036506652832), + new Vec3(0, 3.141640591220885, 0), + new Vec3(26.000009536743164, 0, 0), + [ + [0, 0, 7, 92, 0, 0, 0, 0, 23, 93], + [0, 0, 0, 0, 0, 0], + [18, 128, 125, 68, 9, 0, 160, 66], + [128, 238, 250, 48], + ] + ), + new QuestNpc( + NpcType.Scientist, + 30, + 1, + 0, + 20, + new Vec3(-147.0000457763672, 0, -7.996537208557129), + new Vec3(0, 2.577127047485882, 0), + new Vec3(30.000009536743164, 0, 0), + [ + [0, 0, 7, 93, 0, 0, 0, 0, 23, 94], + [0, 0, 0, 0, 0, 0], + [18, 64, 125, 68, 8, 0, 140, 66], + [128, 238, 254, 176], + ] + ), + new QuestNpc( + NpcType.MaleOld, + 13, + 1, + 0, + 20, + new Vec3(-219.99710083007812, 0, -100.0008316040039), + new Vec3(0, 0, 0), + new Vec3(30.000011444091797, 0, 0), + [ + [0, 0, 7, 94, 0, 0, 0, 0, 23, 95], + [0, 0, 0, 0, 0, 0], + [18, 0, 125, 68, 15, 0, 112, 66], + [128, 239, 3, 48], + ] + ), + new QuestNpc( + NpcType.GuildLady, + 29, + 0, + 0, + 20, + new Vec3(-262.5099792480469, 0, -24.53999900817871), + new Vec3(0, 1.963525369513053, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 95, 0, 0, 0, 0, 23, 106], + [0, 0, 0, 0, 0, 0], + [18, 128, 124, 68, 0, 0, 82, 68], + [128, 239, 100, 192], + ] + ), + new QuestNpc( + NpcType.Tekker, + 28, + 0, + 0, + 30, + new Vec3(-43.70983123779297, 2.5999999046325684, -52.78248596191406), + new Vec3(0, 0.7854101478052212, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 97, 0, 0, 0, 0, 23, 98], + [0, 0, 0, 0, 0, 0], + [0, 64, 124, 68, 0, 128, 79, 68], + [128, 239, 16, 176], + ] + ), + new QuestNpc( + NpcType.MaleMacho, + 12, + 0, + 0, + 30, + new Vec3(0.33990478515625, 2.5999999046325684, -84.71995544433594), + new Vec3(0, 0, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 98, 0, 0, 0, 0, 23, 99], + [0, 0, 0, 0, 0, 0], + [0, 128, 123, 68, 0, 0, 72, 68], + [128, 239, 21, 48], + ] + ), + new QuestNpc( + NpcType.FemaleMacho, + 5, + 0, + 0, + 30, + new Vec3(43.87113952636719, 2.5999996662139893, -74.80299377441406), + new Vec3(0, -0.5645135437350027, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 99, 0, 0, 0, 0, 23, 100], + [0, 0, 0, 0, 0, 0], + [0, 0, 124, 68, 0, 0, 77, 68], + [128, 239, 25, 176], + ] + ), + new QuestNpc( + NpcType.MaleFat, + 11, + 0, + 0, + 30, + new Vec3(75.88380432128906, 2.5999996662139893, -42.69328308105469), + new Vec3(0, -1.0308508189943528, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 100, 0, 0, 0, 0, 23, 101], + [0, 0, 0, 0, 0, 0], + [18, 192, 123, 68, 0, 128, 74, 68], + [128, 239, 30, 48], + ] + ), + new QuestNpc( + NpcType.FemaleTall, + 7, + 1, + 0, + 30, + new Vec3(16.003997802734375, 0, 5.995697021484375), + new Vec3(0, -1.1781152217078317, 0), + new Vec3(22.000009536743164, 0, 0), + [ + [0, 0, 7, 101, 0, 0, 0, 0, 23, 102], + [0, 0, 0, 0, 0, 0], + [18, 64, 127, 68, 4, 0, 12, 67], + [128, 239, 34, 176], + ] + ), + new QuestNpc( + NpcType.Nurse, + 31, + 0, + 0, + 40, + new Vec3(0.3097381591796875, 3, -105.3865966796875), + new Vec3(0, 0, 0), + new Vec3(0, 0, 0), + [ + [0, 0, 7, 102, 0, 0, 0, 0, 23, 103], + [0, 0, 0, 0, 0, 0], + [0, 64, 126, 68, 0, 0, 87, 68], + [128, 239, 39, 48], + ] + ), + new QuestNpc( + NpcType.Nurse, + 31, + 1, + 0, + 40, + new Vec3(53.499176025390625, 0, -26.496688842773438), + new Vec3(0, 5.497871034636549, 0), + new Vec3(18.000009536743164, 0, 0), + [ + [0, 0, 7, 103, 0, 0, 0, 0, 23, 104], + [0, 0, 0, 0, 0, 0], + [18, 128, 126, 68, 7, 0, 220, 66], + [128, 239, 43, 176], + ] + ), + ]; +} diff --git a/src/ui/quest_editor/QuestEditorComponent.css b/src/ui/quest_editor/QuestEditorComponent.css index ad920ba1..d98f6895 100644 --- a/src/ui/quest_editor/QuestEditorComponent.css +++ b/src/ui/quest_editor/QuestEditorComponent.css @@ -3,15 +3,6 @@ flex-direction: column; } -.qe-QuestEditorComponent-toolbar { - display: flex; - padding: 10px 5px; -} - -.qe-QuestEditorComponent-toolbar > * { - margin: 0 5px; -} - .qe-QuestEditorComponent-main { flex: 1; display: flex; diff --git a/src/ui/quest_editor/QuestEditorComponent.tsx b/src/ui/quest_editor/QuestEditorComponent.tsx index 0a9cc0ad..a4692408 100644 --- a/src/ui/quest_editor/QuestEditorComponent.tsx +++ b/src/ui/quest_editor/QuestEditorComponent.tsx @@ -1,31 +1,17 @@ -import { Button, Form, Icon, Input, Modal, Select, Upload } from "antd"; -import { UploadChangeParam } from "antd/lib/upload"; -import { UploadFile } from "antd/lib/upload/interface"; import { observer } from "mobx-react"; -import React, { ChangeEvent, ReactNode, Component } from "react"; +import React, { Component, ReactNode } from "react"; +import { get_quest_renderer } from "../../rendering/QuestRenderer"; +import { application_store } from "../../stores/ApplicationStore"; import { quest_editor_store } from "../../stores/QuestEditorStore"; +import { RendererComponent } from "../RendererComponent"; import { EntityInfoComponent } from "./EntityInfoComponent"; import "./QuestEditorComponent.css"; import { QuestInfoComponent } from "./QuestInfoComponent"; -import { RendererComponent } from "../RendererComponent"; -import { get_quest_renderer } from "../../rendering/QuestRenderer"; -import { application_store } from "../../stores/ApplicationStore"; +import { Toolbar } from "./Toolbar"; @observer -export class QuestEditorComponent extends Component< - {}, - { - debug: boolean; - filename?: string; - save_dialog_open: boolean; - save_dialog_filename: string; - } -> { - state = { - debug: false, - save_dialog_open: false, - save_dialog_filename: "Untitled", - }; +export class QuestEditorComponent extends Component<{}, { debug: boolean }> { + state = { debug: false }; componentDidMount(): void { application_store.on_global_keyup("quest_editor", this.keyup); @@ -36,49 +22,16 @@ export class QuestEditorComponent extends Component< return (
- +
-
); } - private save_as_clicked = (filename?: string) => { - const name = filename - ? filename.endsWith(".qst") - ? filename.slice(0, -4) - : filename - : this.state.save_dialog_filename; - - this.setState({ - save_dialog_open: true, - save_dialog_filename: name, - }); - }; - - private save_dialog_filename_changed = (filename: string) => { - this.setState({ save_dialog_filename: filename }); - }; - - private save_dialog_affirmed = () => { - quest_editor_store.save_current_quest_to_file(this.state.save_dialog_filename); - this.setState({ save_dialog_open: false }); - }; - - private save_dialog_cancelled = () => { - this.setState({ save_dialog_open: false }); - }; - private keyup = (e: KeyboardEvent) => { if (e.ctrlKey && e.key === "z" && !e.altKey) { quest_editor_store.undo_stack.undo(); @@ -89,116 +42,3 @@ export class QuestEditorComponent extends Component< } }; } - -@observer -class Toolbar extends Component<{ on_save_as_clicked: (filename?: string) => void }> { - state = { - filename: undefined, - }; - - render(): ReactNode { - const undo = quest_editor_store.undo_stack; - const quest = quest_editor_store.current_quest; - const areas = quest ? Array.from(quest.area_variants).map(a => a.area) : []; - const area = quest_editor_store.current_area; - const area_id = area && area.id; - - return ( -
- false} - > - - - - -
- ); - } - - private set_filename = (info: UploadChangeParam) => { - if (info.file.originFileObj) { - this.setState({ filename: info.file.name }); - quest_editor_store.load_file(info.file.originFileObj as File); - } - }; - - private save_as = () => { - this.props.on_save_as_clicked(this.state.filename); - }; - - private undo = () => { - quest_editor_store.undo_stack.undo(); - }; - - private redo = () => { - quest_editor_store.undo_stack.redo(); - }; -} - -class SaveAsForm extends React.Component<{ - is_open: boolean; - filename: string; - on_filename_change: (name: string) => void; - on_ok: () => void; - on_cancel: () => void; -}> { - render(): ReactNode { - return ( - - Save as... - - } - visible={this.props.is_open} - onOk={this.props.on_ok} - onCancel={this.props.on_cancel} - > -
- - - -
-
- ); - } - - private name_changed = (e: ChangeEvent) => { - this.props.on_filename_change(e.currentTarget.value); - }; -} diff --git a/src/ui/quest_editor/Toolbar.less b/src/ui/quest_editor/Toolbar.less new file mode 100644 index 00000000..cc8a6596 --- /dev/null +++ b/src/ui/quest_editor/Toolbar.less @@ -0,0 +1,8 @@ +.qe-Toolbar { + display: flex; + padding: 10px 5px; +} + +.qe-Toolbar > * { + margin: 0 5px; +} diff --git a/src/ui/quest_editor/Toolbar.tsx b/src/ui/quest_editor/Toolbar.tsx new file mode 100644 index 00000000..c91725f7 --- /dev/null +++ b/src/ui/quest_editor/Toolbar.tsx @@ -0,0 +1,139 @@ +import { Button, Dropdown, Form, Icon, Input, Menu, Modal, Select, Upload } from "antd"; +import { UploadChangeParam, UploadFile } from "antd/lib/upload/interface"; +import { observer } from "mobx-react"; +import React, { ChangeEvent, Component, ReactNode } from "react"; +import { Episode } from "../../domain"; +import { quest_editor_store } from "../../stores/QuestEditorStore"; +import "./Toolbar.less"; +import { ClickParam } from "antd/lib/menu"; + +@observer +export class Toolbar extends Component { + render(): ReactNode { + const undo = quest_editor_store.undo_stack; + const quest = quest_editor_store.current_quest; + const areas = quest ? Array.from(quest.area_variants).map(a => a.area) : []; + const area = quest_editor_store.current_area; + const area_id = area && area.id; + + return ( +
+ + Episode I + Episode II + Episode IV + + } + trigger={["click"]} + > + + + false} + > + + + + +
+ ); + } + + private new_quest({ key }: ClickParam): void { + quest_editor_store.new_quest((Episode as any)[key]); + } + + private open_file(info: UploadChangeParam): void { + if (info.file.originFileObj) { + quest_editor_store.open_file(info.file.name, info.file.originFileObj as File); + } + } + + private undo(): void { + quest_editor_store.undo_stack.undo(); + } + + private redo(): void { + quest_editor_store.undo_stack.redo(); + } +} + +@observer +class SaveQuestComponent extends Component { + render(): ReactNode { + return ( + + Save as... + + } + visible={quest_editor_store.save_dialog_open} + onOk={this.ok} + onCancel={this.cancel} + > +
+ + + +
+
+ ); + } + + private name_changed(e: ChangeEvent): void { + quest_editor_store.set_save_dialog_filename(e.currentTarget.value); + } + + private ok(): void { + quest_editor_store.save_current_quest_to_file( + quest_editor_store.save_dialog_filename || "untitled" + ); + } + + private cancel(): void { + quest_editor_store.close_save_dialog(); + } +} diff --git a/src/undo.ts b/src/undo.ts index a6f17030..e22fd5d2 100644 --- a/src/undo.ts +++ b/src/undo.ts @@ -1,4 +1,4 @@ -import { computed, observable } from "mobx"; +import { computed, observable, IObservableArray, action } from "mobx"; export class Action { constructor( @@ -9,7 +9,9 @@ export class Action { } export class UndoStack { - @observable.ref private stack: Action[] = []; + @observable private readonly stack: IObservableArray = observable.array([], { + deep: false, + }); /** * The index where new actions are inserted. */ @@ -37,15 +39,18 @@ export class UndoStack { return this.stack[this.index]; } + @action push_action(description: string, undo: () => void, redo: () => void): void { this.push(new Action(description, undo, redo)); } + @action push(action: Action): void { this.stack.splice(this.index, this.stack.length - this.index, action); this.index++; } + @action undo(): boolean { if (this.can_undo) { this.stack[--this.index].undo(); @@ -55,6 +60,7 @@ export class UndoStack { } } + @action redo(): boolean { if (this.can_redo) { this.stack[this.index++].redo(); @@ -64,8 +70,9 @@ export class UndoStack { } } + @action clear(): void { - this.stack = []; + this.stack.clear(); this.index = 0; } } diff --git a/test/resources/quest118_e.bin b/test/resources/quest118_e.bin index 54e78a88..b259a074 100644 Binary files a/test/resources/quest118_e.bin and b/test/resources/quest118_e.bin differ