Wave data is now parsed/written and converted to models.

This commit is contained in:
Daan Vanden Bosch 2019-10-10 13:47:43 +02:00
parent dbe4f3ab78
commit 9803bfe125
18 changed files with 648 additions and 198 deletions

View File

@ -54,14 +54,14 @@ export abstract class AbstractWritableCursor extends AbstractCursor implements W
return this;
}
write_u8_array(array: number[]): this {
write_u8_array(array: readonly number[]): this {
this.ensure_size(array.length);
new Uint8Array(this.backing_buffer, this.offset + this.position).set(new Uint8Array(array));
this._position += array.length;
return this;
}
write_u16_array(array: number[]): this {
write_u16_array(array: readonly number[]): this {
this.ensure_size(2 * array.length);
const len = array.length;
@ -72,7 +72,7 @@ export abstract class AbstractWritableCursor extends AbstractCursor implements W
return this;
}
write_u32_array(array: number[]): this {
write_u32_array(array: readonly number[]): this {
this.ensure_size(4 * array.length);
const len = array.length;

View File

@ -12,7 +12,7 @@ export class ResizableBufferCursor extends AbstractWritableCursor implements Wri
set size(size: number) {
if (size > this._size) {
this.ensure_size(size - this._size);
this.ensure_size(size - this.position);
} else {
this._size = size;
}

View File

@ -30,17 +30,15 @@ import { ResizableBuffer } from "../../ResizableBuffer";
const logger = Logger.get("core/data_formats/parsing/quest/bin");
export class BinFile {
constructor(
readonly quest_id: number,
readonly language: number,
readonly quest_name: string,
readonly short_description: string,
readonly long_description: string,
readonly object_code: Segment[],
readonly shop_items: number[],
) {}
}
export type BinFile = {
readonly quest_id: number;
readonly language: number;
readonly quest_name: string;
readonly short_description: string;
readonly long_description: string;
readonly object_code: readonly Segment[];
readonly shop_items: readonly number[];
};
const SEGMENT_PRIORITY: number[] = [];
SEGMENT_PRIORITY[SegmentType.Instructions] = 2;
@ -82,15 +80,15 @@ export function parse_bin(
const segments = parse_object_code(object_code, label_holder, entry_labels, lenient);
return new BinFile(
return {
quest_id,
language,
quest_name,
short_description,
long_description,
segments,
object_code: segments,
shop_items,
);
};
}
export function write_bin(bin: BinFile): ArrayBuffer {
@ -697,7 +695,7 @@ function parse_instruction_arguments(cursor: Cursor, opcode: Opcode): Arg[] {
function write_object_code(
cursor: WritableCursor,
segments: Segment[],
segments: readonly Segment[],
): { size: number; label_offsets: number[] } {
const start_pos = cursor.position;
// Keep track of label offsets.

View File

@ -2,7 +2,7 @@ import { Endianness } from "../../Endianness";
import { prs_decompress } from "../../compression/prs/decompress";
import { BufferCursor } from "../../cursor/BufferCursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { parse_dat, write_dat } from "./dat";
import { DatFile, parse_dat, write_dat } from "./dat";
import { readFileSync } from "fs";
/**
@ -37,13 +37,25 @@ test("parse, modify and write DAT", () => {
const test_parsed = parse_dat(orig_dat);
orig_dat.seek_start(0);
test_parsed.objs[9].position = {
x: 13,
y: 17,
z: 19,
const test_updated: DatFile = {
...test_parsed,
objs: test_parsed.objs.map((obj, i) => {
if (i === 9) {
return {
...obj,
position: {
x: 13,
y: 17,
z: 19,
},
};
} else {
return obj;
}
}),
};
const test_dat = new ResizableBufferCursor(write_dat(test_parsed), Endianness.Little);
const test_dat = new ResizableBufferCursor(write_dat(test_updated), Endianness.Little);
expect(test_dat.size).toBe(orig_dat.size);

View File

@ -5,51 +5,98 @@ import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { ResizableBuffer } from "../../ResizableBuffer";
import { Vec3 } from "../../vector";
import { WritableCursor } from "../../cursor/WritableCursor";
const logger = Logger.get("data_formats/parsing/quest/dat");
const logger = Logger.get("core/data_formats/parsing/quest/dat");
const OBJECT_SIZE = 68;
const NPC_SIZE = 72;
export type DatFile = {
objs: DatObject[];
npcs: DatNpc[];
unknowns: DatUnknown[];
readonly objs: readonly DatObject[];
readonly npcs: readonly DatNpc[];
readonly waves: readonly DatWave[];
readonly unknowns: readonly DatUnknown[];
};
export type DatEntity = {
type_id: number;
section_id: number;
position: Vec3;
rotation: Vec3;
area_id: number;
unknown: number[][];
readonly type_id: number;
readonly section_id: number;
readonly position: Vec3;
readonly rotation: Vec3;
readonly area_id: number;
readonly unknown: readonly number[][];
};
export type DatObject = DatEntity & {
id: number;
group_id: number;
properties: number[];
readonly id: number;
readonly group_id: number;
readonly properties: readonly number[];
};
export type DatNpc = DatEntity & {
scale: Vec3;
npc_id: number;
script_label: number;
roaming: number;
readonly scale: Vec3;
readonly npc_id: number;
readonly script_label: number;
readonly roaming: number;
};
export type DatWave = {
readonly id: number;
readonly section_id: number;
readonly wave: number;
readonly delay: number;
readonly actions: readonly DatWaveAction[];
readonly area_id: number;
readonly unknown: number;
};
export enum DatWaveActionType {
SpawnNpcs = 0x8,
Unlock = 0xa,
Lock = 0xb,
SpawnWave = 0xc,
}
export type DatWaveAction =
| DatWaveActionSpawnNpcs
| DatWaveActionUnlock
| DatWaveActionLock
| DatWaveActionSpawnWave;
export type DatWaveActionSpawnNpcs = {
readonly type: DatWaveActionType.SpawnNpcs;
readonly section_id: number;
readonly appear_flag: number;
};
export type DatWaveActionUnlock = {
readonly type: DatWaveActionType.Unlock;
readonly door_id: number;
};
export type DatWaveActionLock = {
readonly type: DatWaveActionType.Lock;
readonly door_id: number;
};
export type DatWaveActionSpawnWave = {
readonly type: DatWaveActionType.SpawnWave;
readonly wave_id: number;
};
export type DatUnknown = {
entity_type: number;
total_size: number;
area_id: number;
entities_size: number;
data: number[];
readonly entity_type: number;
readonly total_size: number;
readonly area_id: number;
readonly entities_size: number;
readonly data: number[];
};
export function parse_dat(cursor: Cursor): DatFile {
const objs: DatObject[] = [];
const npcs: DatNpc[] = [];
const waves: DatWave[] = [];
const unknowns: DatUnknown[] = [];
while (cursor.bytes_left) {
@ -68,99 +115,16 @@ export function parse_dat(cursor: Cursor): DatFile {
);
}
const entities_cursor = cursor.take(entities_size);
if (entity_type === 1) {
// Objects
const object_count = Math.floor(entities_size / OBJECT_SIZE);
const start_position = cursor.position;
for (let i = 0; i < object_count; ++i) {
const type_id = cursor.u16();
const unknown1 = cursor.u8_array(6);
const id = cursor.u16();
const group_id = cursor.u16();
const section_id = cursor.u16();
const unknown2 = cursor.u8_array(2);
const position = cursor.vec3_f32();
const rotation = {
x: (cursor.i32() / 0xffff) * 2 * Math.PI,
y: (cursor.i32() / 0xffff) * 2 * Math.PI,
z: (cursor.i32() / 0xffff) * 2 * Math.PI,
};
const properties = [
cursor.f32(),
cursor.f32(),
cursor.f32(),
cursor.u32(),
cursor.u32(),
cursor.u32(),
cursor.u32(),
];
objs.push({
type_id,
id,
group_id,
section_id,
position,
rotation,
properties,
area_id,
unknown: [unknown1, unknown2],
});
}
const bytes_read = cursor.position - start_position;
if (bytes_read !== entities_size) {
logger.warn(
`Read ${bytes_read} bytes instead of expected ${entities_size} for entity type ${entity_type} (Object).`,
);
cursor.seek(entities_size - bytes_read);
}
parse_objects(entities_cursor, area_id, objs);
} else if (entity_type === 2) {
// NPCs
const npc_count = Math.floor(entities_size / NPC_SIZE);
const start_position = cursor.position;
for (let i = 0; i < npc_count; ++i) {
const type_id = cursor.u16();
const unknown1 = cursor.u8_array(10);
const section_id = cursor.u16();
const unknown2 = cursor.u8_array(6);
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 scale = cursor.vec3_f32();
const npc_id = cursor.f32();
const script_label = cursor.f32();
const roaming = cursor.u32();
const unknown3 = cursor.u8_array(4);
npcs.push({
type_id,
section_id,
position,
rotation: { x: rotation_x, y: rotation_y, z: rotation_z },
scale,
npc_id,
script_label,
roaming,
area_id,
unknown: [unknown1, unknown2, unknown3],
});
}
const bytes_read = cursor.position - start_position;
if (bytes_read !== entities_size) {
logger.warn(
`Read ${bytes_read} bytes instead of expected ${entities_size} for entity type ${entity_type} (NPC).`,
);
cursor.seek(entities_size - bytes_read);
}
parse_npcs(entities_cursor, area_id, npcs);
} else if (entity_type === 3) {
parse_waves(entities_cursor, area_id, waves);
} else {
// There are also waves (type 3) and unknown entity types 4 and 5.
// Unknown entity types 4 and 5.
unknowns.push({
entity_type,
total_size,
@ -169,13 +133,19 @@ export function parse_dat(cursor: Cursor): DatFile {
data: cursor.u8_array(entities_size),
});
}
if (entities_cursor.bytes_left) {
logger.warn(
`Read ${entities_cursor.position} bytes instead of expected ${entities_cursor.size} for entity type ${entity_type}.`,
);
}
}
}
return { objs, npcs, unknowns };
return { objs, npcs, waves, unknowns };
}
export function write_dat({ objs, npcs, unknowns }: DatFile): ResizableBuffer {
export function write_dat({ objs, npcs, waves, unknowns }: DatFile): ResizableBuffer {
const buffer = new ResizableBuffer(
objs.length * (16 + OBJECT_SIZE) +
npcs.length * (16 + NPC_SIZE) +
@ -183,6 +153,214 @@ export function write_dat({ objs, npcs, unknowns }: DatFile): ResizableBuffer {
);
const cursor = new ResizableBufferCursor(buffer, Endianness.Little);
write_objects(cursor, objs);
write_npcs(cursor, npcs);
write_waves(cursor, waves);
for (const unknown of unknowns) {
cursor.write_u32(unknown.entity_type);
cursor.write_u32(unknown.total_size);
cursor.write_u32(unknown.area_id);
cursor.write_u32(unknown.entities_size);
cursor.write_u8_array(unknown.data);
}
// Final header.
cursor.write_u32(0);
cursor.write_u32(0);
cursor.write_u32(0);
cursor.write_u32(0);
return buffer;
}
function parse_objects(cursor: Cursor, area_id: number, objs: DatObject[]): void {
const object_count = Math.floor(cursor.size / OBJECT_SIZE);
for (let i = 0; i < object_count; ++i) {
const type_id = cursor.u16();
const unknown1 = cursor.u8_array(6);
const id = cursor.u16();
const group_id = cursor.u16();
const section_id = cursor.u16();
const unknown2 = cursor.u8_array(2);
const position = cursor.vec3_f32();
const rotation = {
x: (cursor.i32() / 0xffff) * 2 * Math.PI,
y: (cursor.i32() / 0xffff) * 2 * Math.PI,
z: (cursor.i32() / 0xffff) * 2 * Math.PI,
};
const properties = [
cursor.f32(),
cursor.f32(),
cursor.f32(),
cursor.u32(),
cursor.u32(),
cursor.u32(),
cursor.u32(),
];
objs.push({
type_id,
id,
group_id,
section_id,
position,
rotation,
properties,
area_id,
unknown: [unknown1, unknown2],
});
}
}
function parse_npcs(cursor: Cursor, area_id: number, npcs: DatNpc[]): void {
const npc_count = Math.floor(cursor.size / NPC_SIZE);
for (let i = 0; i < npc_count; ++i) {
const type_id = cursor.u16();
const unknown1 = cursor.u8_array(10);
const section_id = cursor.u16();
const unknown2 = cursor.u8_array(6);
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 scale = cursor.vec3_f32();
const npc_id = cursor.f32();
const script_label = cursor.f32();
const roaming = cursor.u32();
const unknown3 = cursor.u8_array(4);
npcs.push({
type_id,
section_id,
position,
rotation: { x: rotation_x, y: rotation_y, z: rotation_z },
scale,
npc_id,
script_label,
roaming,
area_id,
unknown: [unknown1, unknown2, unknown3],
});
}
}
function parse_waves(cursor: Cursor, area_id: number, waves: DatWave[]): void {
const actions_offset = cursor.u32();
cursor.seek(4); // Always 0x10
const wave_count = cursor.u32();
cursor.seek(3); // Always 0
const wave_type = cursor.u8();
if (wave_type === 0x32) {
throw new Error("Can't parse challenge mode quests yet.");
}
cursor.seek_start(actions_offset);
const actions_cursor = cursor.take(cursor.bytes_left);
cursor.seek_start(16);
for (let i = 0; i < wave_count; ++i) {
const id = cursor.u32();
cursor.seek(4); // Always 0x100
const section_id = cursor.u16();
const wave = cursor.u16();
const delay = cursor.u16();
const unknown = cursor.u16(); // "wavesetting"?
const wave_actions_offset = cursor.u32();
actions_cursor.seek_start(wave_actions_offset);
const actions = parse_wave_actions(actions_cursor);
waves.push({
id,
section_id,
wave,
delay,
actions,
area_id,
unknown,
});
}
if (cursor.position !== actions_offset) {
logger.warn(
`Read ${cursor.position - 16} bytes of wave data instead of expected ${actions_offset -
16}.`,
);
}
let last_u8 = 0xff;
while (actions_cursor.bytes_left) {
last_u8 = actions_cursor.u8();
if (last_u8 !== 0xff) {
break;
}
}
if (last_u8 !== 0xff) {
actions_cursor.seek(-1);
}
// Make sure the cursor position represents the amount of bytes we've consumed.
cursor.seek_start(actions_offset + actions_cursor.position);
}
function parse_wave_actions(cursor: Cursor): DatWaveAction[] {
const actions: DatWaveAction[] = [];
outer: while (cursor.bytes_left) {
const type = cursor.u8();
switch (type) {
case 1:
break outer;
case DatWaveActionType.SpawnNpcs:
actions.push({
type: DatWaveActionType.SpawnNpcs,
section_id: cursor.u16(),
appear_flag: cursor.u16(),
});
break;
case DatWaveActionType.Unlock:
actions.push({
type: DatWaveActionType.Unlock,
door_id: cursor.u16(),
});
break;
case DatWaveActionType.Lock:
actions.push({
type: DatWaveActionType.Lock,
door_id: cursor.u16(),
});
break;
case DatWaveActionType.SpawnWave:
actions.push({
type: DatWaveActionType.SpawnWave,
wave_id: cursor.u32(),
});
break;
default:
logger.warn(`Unexpected wave action type ${type}.`);
break outer;
}
}
return actions;
}
function write_objects(cursor: WritableCursor, objs: readonly DatObject[]): void {
const grouped_objs = groupBy(objs, obj => obj.area_id);
const obj_area_ids = Object.keys(grouped_objs)
.map(key => parseInt(key, 10))
@ -231,7 +409,9 @@ export function write_dat({ objs, npcs, unknowns }: DatFile): ResizableBuffer {
cursor.write_u32(obj.properties[6]);
}
}
}
function write_npcs(cursor: WritableCursor, npcs: readonly DatNpc[]): void {
const grouped_npcs = groupBy(npcs, npc => npc.area_id);
const npc_area_ids = Object.keys(grouped_npcs)
.map(key => parseInt(key, 10))
@ -276,20 +456,101 @@ export function write_dat({ objs, npcs, unknowns }: DatFile): ResizableBuffer {
cursor.write_u8_array(npc.unknown[2]);
}
}
for (const unknown of unknowns) {
cursor.write_u32(unknown.entity_type);
cursor.write_u32(unknown.total_size);
cursor.write_u32(unknown.area_id);
cursor.write_u32(unknown.entities_size);
cursor.write_u8_array(unknown.data);
}
// Final header.
cursor.write_u32(0);
cursor.write_u32(0);
cursor.write_u32(0);
cursor.write_u32(0);
return buffer;
}
function write_waves(cursor: WritableCursor, waves: readonly DatWave[]): void {
const grouped_waves = groupBy(waves, wave => wave.area_id);
const wave_area_ids = Object.keys(grouped_waves)
.map(key => parseInt(key, 10))
.sort((a, b) => a - b);
for (const area_id of wave_area_ids) {
const area_waves = grouped_waves[area_id];
// Standard header.
cursor.write_u32(3); // Entity type
const total_size_offset = cursor.position;
cursor.write_u32(0); // Placeholder for the total size.
cursor.write_u32(area_id);
const entities_size_offset = cursor.position;
cursor.write_u32(0); // Placeholder for the entities size.
// Wave header.
const start_pos = cursor.position;
// TODO: actual wave size is dependent on the wave type (challenge mode).
// Absolute offset.
const actions_offset = start_pos + 16 + 20 * area_waves.length;
cursor.size = Math.max(actions_offset, cursor.size);
cursor.write_u32(actions_offset - start_pos);
cursor.write_u32(0x10);
cursor.write_u32(area_waves.length);
cursor.write_u32(0); // TODO: write wave type (challenge mode).
// Relative offset.
let wave_actions_offset = 0;
for (const wave of area_waves) {
cursor.write_u32(wave.id);
cursor.write_u32(0x10000);
cursor.write_u16(wave.section_id);
cursor.write_u16(wave.wave);
cursor.write_u16(wave.delay);
cursor.write_u16(wave.unknown);
cursor.write_u32(wave_actions_offset);
const next_wave_pos = cursor.position;
cursor.seek_start(actions_offset + wave_actions_offset);
for (const action of wave.actions) {
cursor.write_u8(action.type);
switch (action.type) {
case DatWaveActionType.SpawnNpcs:
cursor.write_u16(action.section_id);
cursor.write_u16(action.appear_flag);
break;
case DatWaveActionType.Unlock:
cursor.write_u16(action.door_id);
break;
case DatWaveActionType.Lock:
cursor.write_u16(action.door_id);
break;
case DatWaveActionType.SpawnWave:
cursor.write_u32(action.wave_id);
break;
default:
// Need to cast because TypeScript infers action to be `never`.
throw new Error(`Unknown wave action type ${(action as any).type}.`);
}
}
// End of wave actions.
cursor.write_u8(1);
wave_actions_offset = cursor.position - actions_offset;
cursor.seek_start(next_wave_pos);
}
cursor.seek_start(actions_offset + wave_actions_offset);
while ((cursor.position - actions_offset) % 4 !== 0) {
cursor.write_u8(0xff);
}
const end_pos = cursor.position;
cursor.seek_start(total_size_offset);
cursor.write_u32(end_pos - start_pos + 16);
cursor.seek_start(entities_size_offset);
cursor.write_u32(end_pos - start_pos);
cursor.seek_start(end_pos);
}
}

View File

@ -1,8 +1,7 @@
import { Vec3 } from "../../vector";
import { npc_data, NpcType, NpcTypeData } from "./npc_types";
import { object_data, ObjectType, ObjectTypeData } from "./object_types";
export type QuestEntity = QuestNpc | QuestObject;
import { DatWave } from "./dat";
export type QuestNpc = {
readonly type: NpcType;
@ -20,7 +19,7 @@ export type QuestNpc = {
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: number[][];
readonly unknown: readonly number[][];
readonly pso_type_id: number;
readonly npc_id: number;
readonly script_label: number;
@ -45,9 +44,11 @@ export type QuestObject = {
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: number[][];
readonly unknown: readonly number[][];
};
export type QuestWave = DatWave;
export type EntityTypeData = NpcTypeData | ObjectTypeData;
export type EntityType = NpcType | ObjectType;

View File

@ -12,9 +12,9 @@ import { ArrayBufferCursor } from "../../cursor/ArrayBufferCursor";
import { Cursor } from "../../cursor/Cursor";
import { ResizableBufferCursor } from "../../cursor/ResizableBufferCursor";
import { Endianness } from "../../Endianness";
import { BinFile, parse_bin, write_bin } from "./bin";
import { parse_bin, write_bin } from "./bin";
import { DatFile, DatNpc, DatObject, DatUnknown, parse_dat, write_dat } from "./dat";
import { QuestNpc, QuestObject } from "./entities";
import { QuestNpc, QuestObject, QuestWave } from "./entities";
import { Episode } from "./Episode";
import { object_data, ObjectType, pso_id_to_object_type } from "./object_types";
import { parse_qst, QstContainedFile, write_qst } from "./qst";
@ -29,14 +29,15 @@ export type Quest = {
readonly short_description: string;
readonly long_description: string;
readonly episode: Episode;
readonly objects: QuestObject[];
readonly npcs: QuestNpc[];
readonly objects: readonly QuestObject[];
readonly npcs: readonly QuestNpc[];
readonly waves: readonly QuestWave[];
/**
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
*/
readonly dat_unknowns: DatUnknown[];
readonly object_code: Segment[];
readonly shop_items: number[];
readonly dat_unknowns: readonly DatUnknown[];
readonly object_code: readonly Segment[];
readonly shop_items: readonly number[];
readonly map_designations: Map<number, number>;
};
@ -125,6 +126,7 @@ export function parse_quest(cursor: Cursor, lenient: boolean = false): Quest | u
episode,
objects,
npcs: parse_npc_data(episode, dat.npcs),
waves: dat.waves,
dat_unknowns: dat.unknowns,
object_code: bin.object_code,
shop_items: bin.shop_items,
@ -136,19 +138,18 @@ export function write_quest_qst(quest: Quest, file_name: string): ArrayBuffer {
const dat = write_dat({
objs: objects_to_dat_data(quest.objects),
npcs: npcs_to_dat_data(quest.npcs),
waves: quest.waves,
unknowns: quest.dat_unknowns,
});
const bin = write_bin(
new BinFile(
quest.id,
quest.language,
quest.name,
quest.short_description,
quest.long_description,
quest.object_code,
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: quest.object_code,
shop_items: quest.shop_items,
});
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));
@ -211,7 +212,10 @@ function extract_map_designations(
return map_designations;
}
function extract_script_entry_points(objects: QuestObject[], npcs: DatNpc[]): number[] {
function extract_script_entry_points(
objects: readonly QuestObject[],
npcs: readonly DatNpc[],
): number[] {
const entry_points = new Set([0]);
for (const obj of objects) {
@ -235,7 +239,7 @@ function extract_script_entry_points(objects: QuestObject[], npcs: DatNpc[]): nu
return [...entry_points];
}
function parse_obj_data(objs: DatObject[]): QuestObject[] {
function parse_obj_data(objs: readonly DatObject[]): QuestObject[] {
return objs.map(obj_data => {
const type = pso_id_to_object_type(obj_data.type_id);
@ -270,7 +274,7 @@ function parse_obj_data(objs: DatObject[]): QuestObject[] {
});
}
function parse_npc_data(episode: number, npcs: DatNpc[]): QuestNpc[] {
function parse_npc_data(episode: number, npcs: readonly DatNpc[]): QuestNpc[] {
return npcs.map(npc_data => {
return {
type: get_npc_type(episode, npc_data),
@ -564,7 +568,7 @@ function get_npc_type(episode: number, { type_id, scale, roaming, area_id }: Dat
return NpcType.Unknown;
}
function objects_to_dat_data(objects: QuestObject[]): DatObject[] {
function objects_to_dat_data(objects: readonly QuestObject[]): DatObject[] {
return objects.map(object => ({
type_id: object_data(object.type).pso_id!,
id: object.id,
@ -578,7 +582,8 @@ function objects_to_dat_data(objects: QuestObject[]): DatObject[] {
}));
}
function npcs_to_dat_data(npcs: QuestNpc[]): DatNpc[] {
function npcs_to_dat_data(npcs: readonly QuestNpc[]): DatNpc[] {
// TODO: use primitive reinterpretation functions instead of DataView.
const dv = new DataView(new ArrayBuffer(4));
return npcs.map(npc => {

View File

@ -18,4 +18,9 @@ export class AreaVariantModel {
this.id = id;
this.area = area;
}
set_sections(sections: readonly SectionModel[]): this {
this._sections.val = sections;
return this;
}
}

View File

@ -13,6 +13,7 @@ import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { QuestEntityModel } from "./QuestEntityModel";
import { entity_type_to_string } from "../../core/data_formats/parsing/quest/entities";
import { QuestWaveModel } from "./QuestWaveModel";
const logger = Logger.get("quest_editor/model/QuestModel");
@ -26,6 +27,7 @@ export class QuestModel {
private readonly _area_variants: WritableListProperty<AreaVariantModel> = list_property();
private readonly _objects: WritableListProperty<QuestObjectModel>;
private readonly _npcs: WritableListProperty<QuestNpcModel>;
private readonly _waves: WritableListProperty<QuestWaveModel>;
readonly id: Property<number> = this._id;
@ -58,6 +60,8 @@ export class QuestModel {
readonly npcs: ListProperty<QuestNpcModel>;
readonly waves: ListProperty<QuestWaveModel>;
/**
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
*/
@ -75,16 +79,18 @@ export class QuestModel {
long_description: string,
episode: Episode,
map_designations: Map<number, number>,
objects: QuestObjectModel[],
npcs: QuestNpcModel[],
dat_unknowns: DatUnknown[],
object_code: Segment[],
shop_items: number[],
objects: readonly QuestObjectModel[],
npcs: readonly QuestNpcModel[],
waves: readonly QuestWaveModel[],
dat_unknowns: readonly DatUnknown[],
object_code: readonly Segment[],
shop_items: readonly number[],
) {
check_episode(episode);
if (!map_designations) throw new Error("map_designations is required.");
if (!Array.isArray(objects)) throw new Error("objs is required.");
if (!Array.isArray(npcs)) throw new Error("npcs is required.");
if (!Array.isArray(waves)) throw new Error("waves is required.");
if (!Array.isArray(dat_unknowns)) throw new Error("dat_unknowns is required.");
if (!Array.isArray(object_code)) throw new Error("object_code is required.");
if (!Array.isArray(shop_items)) throw new Error("shop_items is required.");
@ -101,6 +107,8 @@ export class QuestModel {
this.objects = this._objects;
this._npcs = list_property(undefined, ...npcs);
this.npcs = this._npcs;
this._waves = list_property(undefined, ...waves);
this.waves = this._waves;
this.dat_unknowns = dat_unknowns;
this.object_code = object_code;
this.shop_items = shop_items;

View File

@ -11,7 +11,7 @@ export class QuestNpcModel extends QuestEntityModel<NpcType> {
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: number[][];
readonly unknown: readonly number[][];
constructor(
type: NpcType,
@ -24,7 +24,7 @@ export class QuestNpcModel extends QuestEntityModel<NpcType> {
position: Vector3,
rotation: Euler,
scale: Vector3,
unknown: number[][],
unknown: readonly number[][],
) {
if (!Number.isInteger(pso_type_id)) throw new Error("pso_type_id should be an integer.");
if (!Number.isFinite(npc_id)) throw new Error("npc_id should be a number.");

View File

@ -9,7 +9,7 @@ export class QuestObjectModel extends QuestEntityModel<ObjectType> {
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: number[][];
readonly unknown: readonly number[][];
constructor(
type: ObjectType,
@ -20,7 +20,7 @@ export class QuestObjectModel extends QuestEntityModel<ObjectType> {
position: Vector3,
rotation: Euler,
properties: Map<string, number>,
unknown: number[][],
unknown: readonly number[][],
) {
super(type, area_id, section_id, position, rotation);

View File

@ -0,0 +1,43 @@
export abstract class QuestWaveActionModel {}
export class QuestWaveActionSpawnNpcsModel extends QuestWaveActionModel {
readonly section_id: number;
readonly appear_flag: number;
constructor(section_id: number, appear_flag: number) {
super();
this.section_id = section_id;
this.appear_flag = appear_flag;
}
}
export class QuestWaveActionUnlockModel extends QuestWaveActionModel {
readonly door_id: number;
constructor(door_id: number) {
super();
this.door_id = door_id;
}
}
export class QuestWaveActionLockModel extends QuestWaveActionModel {
readonly door_id: number;
constructor(door_id: number) {
super();
this.door_id = door_id;
}
}
export class QuestWaveActionSpawnWaveModel extends QuestWaveActionModel {
readonly wave_id: number;
constructor(wave_id: number) {
super();
this.wave_id = wave_id;
}
}

View File

@ -0,0 +1,37 @@
import { QuestWaveActionModel } from "./QuestWaveActionModel";
export class QuestWaveModel {
readonly id: number;
readonly section_id: number;
readonly wave: number;
readonly delay: number;
readonly actions: readonly QuestWaveActionModel[];
readonly area_id: number;
readonly unknown: number;
constructor(
id: number,
section_id: number,
wave: number,
delay: number,
actions: readonly QuestWaveActionModel[],
area_id: number,
unknown: number,
) {
if (!Number.isInteger(id)) throw new Error("id should be an integer.");
if (!Number.isInteger(section_id)) throw new Error("section_id should be an integer.");
if (!Number.isInteger(wave)) throw new Error("wave should be an integer.");
if (!Number.isInteger(delay)) throw new Error("delay should be an integer.");
if (!Array.isArray(actions)) throw new Error("actions should be an array.");
if (!Number.isInteger(area_id)) throw new Error("area_id should be an integer.");
if (!Number.isInteger(unknown)) throw new Error("unknown should be an integer.");
this.id = id;
this.section_id = section_id;
this.wave = wave;
this.delay = delay;
this.actions = actions;
this.area_id = area_id;
this.unknown = unknown;
}
}

View File

@ -117,9 +117,7 @@ test("assembling disassembled object code with manual stack management should re
expect(errors).toEqual([]);
expect(warnings).toEqual([]);
bin.object_code.splice(0, bin.object_code.length, ...object_code);
const test_bytes = new ArrayBufferCursor(write_bin(bin), Endianness.Little);
const test_bytes = new ArrayBufferCursor(write_bin({ ...bin, object_code }), Endianness.Little);
orig_bytes.seek_start(0);
expect(test_bytes.size).toBe(orig_bytes.size);

View File

@ -16,7 +16,7 @@ type ArgWithType = Arg & {
* @param object_code - The object code to disassemble.
* @param manual_stack - If true, will output stack management instructions (argpush variants). Otherwise the arguments of stack management instructions will be output as arguments to the instruction that pops them from the stack.
*/
export function disassemble(object_code: Segment[], manual_stack = false): string[] {
export function disassemble(object_code: readonly Segment[], manual_stack = false): string[] {
logger.trace("disassemble start");
const lines: string[] = [];

View File

@ -160,6 +160,6 @@ function segments_equal(a: Segment, b: Segment): boolean {
}
}
export function segment_arrays_equal(a: Segment[], b: Segment[]): boolean {
export function segment_arrays_equal(a: readonly Segment[], b: readonly Segment[]): boolean {
return arrays_equal(a, b, segments_equal);
}

View File

@ -28,7 +28,15 @@ import { RemoveEntityAction } from "../actions/RemoveEntityAction";
import { Euler, Vector3 } from "three";
import { vec3_to_threejs } from "../../core/rendering/conversion";
import { RotateEntityAction } from "../actions/RotateEntityAction";
import { VirtualMachine, ExecutionResult } from "../scripting/vm";
import { ExecutionResult, VirtualMachine } from "../scripting/vm";
import { QuestWaveModel } from "../model/QuestWaveModel";
import { DatWaveActionType } from "../../core/data_formats/parsing/quest/dat";
import {
QuestWaveActionLockModel,
QuestWaveActionSpawnNpcsModel,
QuestWaveActionSpawnWaveModel,
QuestWaveActionUnlockModel,
} from "../model/QuestWaveActionModel";
import Logger = require("js-logger");
const logger = Logger.get("quest_editor/gui/QuestEditorStore");
@ -160,6 +168,36 @@ export class QuestEditorStore implements Disposable {
npc.unknown,
),
),
quest.waves.map(
wave =>
new QuestWaveModel(
wave.id,
wave.section_id,
wave.wave,
wave.delay,
wave.actions.map(action => {
switch (action.type) {
case DatWaveActionType.SpawnNpcs:
return new QuestWaveActionSpawnNpcsModel(
action.section_id,
action.appear_flag,
);
case DatWaveActionType.Unlock:
return new QuestWaveActionUnlockModel(
action.door_id,
);
case DatWaveActionType.Lock:
return new QuestWaveActionLockModel(action.door_id);
case DatWaveActionType.SpawnWave:
return new QuestWaveActionSpawnWaveModel(
action.wave_id,
);
}
}),
wave.area_id,
wave.unknown,
),
),
quest.dat_unknowns,
quest.object_code,
quest.shop_items,
@ -217,6 +255,44 @@ export class QuestEditorStore implements Disposable {
script_label: npc.script_label,
pso_roaming: npc.pso_roaming,
})),
waves: quest.waves.val.map(wave => ({
id: wave.id,
section_id: wave.section_id,
wave: wave.wave,
delay: wave.delay,
actions: wave.actions.map(action => {
if (action instanceof QuestWaveActionSpawnNpcsModel) {
return {
type: DatWaveActionType.SpawnNpcs,
section_id: action.section_id,
appear_flag: action.appear_flag,
};
} else if (action instanceof QuestWaveActionUnlockModel) {
return {
type: DatWaveActionType.Unlock,
door_id: action.door_id,
};
} else if (action instanceof QuestWaveActionLockModel) {
return {
type: DatWaveActionType.Lock,
door_id: action.door_id,
};
} else if (action instanceof QuestWaveActionSpawnWaveModel) {
return {
type: DatWaveActionType.SpawnWave,
wave_id: action.wave_id,
};
} else {
throw new Error(
`Unknown wave action type ${
Object.getPrototypeOf(action).constructor
}`,
);
}
}),
area_id: wave.area_id,
unknown: wave.unknown,
})),
dat_unknowns: quest.dat_unknowns,
object_code: quest.object_code,
shop_items: quest.shop_items,
@ -316,7 +392,7 @@ export class QuestEditorStore implements Disposable {
// Load section data.
for (const variant of quest.area_variants.val) {
const sections = await area_store.get_area_sections(quest.episode, variant);
variant.sections.val.splice(0, Infinity, ...sections);
variant.set_sections(sections);
for (const object of quest.objects.val.filter(o => o.area_id === variant.area.id)) {
try {

View File

@ -17,6 +17,7 @@ import {
import { QuestObjectModel } from "../model/QuestObjectModel";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { Euler, Vector3 } from "three";
import { QuestWaveModel } from "../model/QuestWaveModel";
export function create_new_quest(episode: Episode): QuestModel {
if (episode === Episode.II) throw new Error("Episode II not yet supported.");
@ -32,6 +33,7 @@ export function create_new_quest(episode: Episode): QuestModel {
new Map().set(0, 0),
create_default_objects(),
create_default_npcs(),
create_default_waves(),
[],
[
{
@ -804,3 +806,7 @@ function create_default_npcs(): QuestNpcModel[] {
),
];
}
function create_default_waves(): QuestWaveModel[] {
return [];
}