Quest entities are now backed by an ArrayBufferBlock so that a "custom entity properties" feature can be added later.

This commit is contained in:
Daan Vanden Bosch 2020-07-18 23:00:48 +02:00
parent cd67e214f1
commit 8b8e87c8c5
46 changed files with 1021 additions and 2016 deletions

View File

@ -173,3 +173,4 @@ Features that are in ***bold italics*** are planned but not yet implemented.
- Desert Fixed Type Box (Breakable Crystals)
- Merissa A
- Merissa AA
- All "Sonic" objects, even the ones that aren't actually Sonic, are rendered as Sonic

Binary file not shown.

View File

@ -9,10 +9,16 @@ export class ArrayBufferBlock extends AbstractWritableBlock {
protected readonly buffer: ArrayBuffer;
protected readonly data_view: DataView;
constructor(size: number, endianness: Endianness) {
get backing_buffer(): ArrayBuffer {
return this.buffer;
}
constructor(buffer_or_size: ArrayBuffer | number, endianness: Endianness) {
super(endianness);
this.buffer = new ArrayBuffer(size);
this.buffer =
typeof buffer_or_size === "number" ? new ArrayBuffer(buffer_or_size) : buffer_or_size;
this.data_view = new DataView(this.buffer);
}
}

View File

@ -40,8 +40,12 @@ export class BufferCursor extends AbstractArrayBufferCursor {
}
take(size: number): BufferCursor {
const offset = this.offset + this.position;
const wrapper = new BufferCursor(this.buffer, this.endianness, offset, size);
const wrapper = new BufferCursor(
this.buffer,
this.endianness,
this.absolute_position - this.buffer.byteOffset,
size,
);
this._position += size;
return wrapper;
}

View File

@ -0,0 +1,338 @@
import { Vec3 } from "../../vector";
import { npc_data, NpcType, NpcTypeData } from "./npc_types";
import { id_to_object_type, object_data, ObjectType, ObjectTypeData } from "./object_types";
import { DatEvent, DatUnknown, NPC_BYTE_SIZE, OBJECT_BYTE_SIZE } from "./dat";
import { Episode } from "./Episode";
import { Segment } from "../../asm/instructions";
import { get_npc_type } from "./get_npc_type";
import { ArrayBufferBlock } from "../../block/ArrayBufferBlock";
import { assert } from "../../../util";
import { Endianness } from "../../block/Endianness";
const DEFAULT_SCALE: Vec3 = Object.freeze({ x: 1, y: 1, z: 1 });
export class Quest {
constructor(
public id: number,
public language: number,
public name: string,
public short_description: string,
public long_description: string,
public episode: Episode,
readonly objects: readonly QuestObject[],
readonly npcs: readonly QuestNpc[],
readonly events: QuestEvent[],
/**
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
*/
readonly dat_unknowns: DatUnknown[],
readonly object_code: readonly Segment[],
readonly shop_items: number[],
readonly map_designations: Map<number, number>,
) {}
}
export type EntityTypeData = NpcTypeData | ObjectTypeData;
export type EntityType = NpcType | ObjectType;
export interface QuestEntity<Type extends EntityType = EntityType> {
area_id: number;
readonly data: ArrayBufferBlock;
type: Type;
section_id: number;
position: Vec3;
rotation: Vec3;
}
export class QuestNpc implements QuestEntity<NpcType> {
episode: Episode;
area_id: number;
readonly data: ArrayBufferBlock;
get type(): NpcType {
return get_npc_type(this.episode, this.type_id, this.regular, this.skin, this.area_id);
}
set type(type: NpcType) {
const data = npc_data(type);
if (data.episode != undefined) {
this.episode = data.episode;
}
this.type_id = data.type_id ?? 0;
this.regular = data.regular ?? true;
this.skin = data.skin ?? 0;
if (data.area_ids.length > 0 && !data.area_ids.includes(this.area_id)) {
this.area_id = data.area_ids[0];
}
}
get type_id(): number {
return this.data.get_u16(0);
}
set type_id(type_id: number) {
this.data.set_u16(0, type_id);
}
get section_id(): number {
return this.data.get_u16(12);
}
set section_id(section_id: number) {
this.data.set_u16(12, section_id);
}
get wave(): number {
return this.data.get_u16(14);
}
set wave(wave: number) {
this.data.set_u16(14, wave);
}
get wave_2(): number {
return this.data.get_u32(16);
}
set wave_2(wave_2: number) {
this.data.set_u32(16, wave_2);
}
/**
* Section-relative position.
*/
get position(): Vec3 {
return {
x: this.data.get_f32(20),
y: this.data.get_f32(24),
z: this.data.get_f32(28),
};
}
set position(position: Vec3) {
this.data.set_f32(20, position.x);
this.data.set_f32(24, position.y);
this.data.set_f32(28, position.z);
}
get rotation(): Vec3 {
return {
x: (this.data.get_i32(32) / 0xffff) * 2 * Math.PI,
y: (this.data.get_i32(36) / 0xffff) * 2 * Math.PI,
z: (this.data.get_i32(40) / 0xffff) * 2 * Math.PI,
};
}
set rotation(rotation: Vec3) {
this.data.set_i32(32, Math.round((rotation.x / (2 * Math.PI)) * 0xffff));
this.data.set_i32(36, Math.round((rotation.y / (2 * Math.PI)) * 0xffff));
this.data.set_i32(40, Math.round((rotation.z / (2 * Math.PI)) * 0xffff));
}
/**
* Seemingly 3 floats, not sure what they represent.
* The y component is used to help determine what the NpcType is.
*/
get scale(): Vec3 {
return {
x: this.data.get_f32(44),
y: this.data.get_f32(48),
z: this.data.get_f32(52),
};
}
set scale(scale: Vec3) {
this.data.set_f32(44, scale.x);
this.data.set_f32(48, scale.y);
this.data.set_f32(52, scale.z);
}
get regular(): boolean {
return Math.abs(this.data.get_f32(48) - 1) > 0.00001;
}
set regular(regular: boolean) {
this.data.set_i32(48, (this.data.get_i32(48) & ~0x800000) | (regular ? 0 : 0x800000));
}
get npc_id(): number {
return this.data.get_f32(56);
}
/**
* Only seems to be valid for non-enemies.
*/
get script_label(): number {
return Math.round(this.data.get_f32(60));
}
get skin(): number {
return this.data.get_u32(64);
}
set skin(skin: number) {
this.data.set_u32(64, skin);
}
constructor(episode: Episode, area_id: number, data: ArrayBufferBlock) {
assert(
data.size === NPC_BYTE_SIZE,
() => `Data size should be ${NPC_BYTE_SIZE} but was ${data.size}.`,
);
this.episode = episode;
this.area_id = area_id;
this.data = data;
}
static create(type: NpcType, area_id: number, wave: number): QuestNpc {
const npc = new QuestNpc(
Episode.I,
area_id,
new ArrayBufferBlock(NPC_BYTE_SIZE, Endianness.Little),
);
// Set scale before type because type will change it.
npc.scale = DEFAULT_SCALE;
npc.type = type;
// Set area_id after type, because you might want to overwrite the area_id that type has
// determined.
npc.area_id = area_id;
npc.wave = wave;
npc.wave_2 = wave;
return npc;
}
}
export class QuestObject implements QuestEntity<ObjectType> {
area_id: number;
readonly data: ArrayBufferBlock;
get type(): ObjectType {
return id_to_object_type(this.type_id);
}
set type(type: ObjectType) {
this.type_id = object_data(type).type_id ?? 0;
}
get type_id(): number {
return this.data.get_u16(0);
}
set type_id(type_id: number) {
this.data.set_u16(0, type_id);
}
get id(): number {
return this.data.get_u16(8);
}
get group_id(): number {
return this.data.get_u16(10);
}
get section_id(): number {
return this.data.get_u16(12);
}
set section_id(section_id: number) {
this.data.set_u16(12, section_id);
}
/**
* Section-relative position.
*/
get position(): Vec3 {
return {
x: this.data.get_f32(16),
y: this.data.get_f32(20),
z: this.data.get_f32(24),
};
}
set position(position: Vec3) {
this.data.set_f32(16, position.x);
this.data.set_f32(20, position.y);
this.data.set_f32(24, position.z);
}
get rotation(): Vec3 {
return {
x: (this.data.get_i32(28) / 0xffff) * 2 * Math.PI,
y: (this.data.get_i32(32) / 0xffff) * 2 * Math.PI,
z: (this.data.get_i32(36) / 0xffff) * 2 * Math.PI,
};
}
set rotation(rotation: Vec3) {
this.data.set_i32(28, Math.round((rotation.x / (2 * Math.PI)) * 0xffff));
this.data.set_i32(32, Math.round((rotation.y / (2 * Math.PI)) * 0xffff));
this.data.set_i32(36, Math.round((rotation.z / (2 * Math.PI)) * 0xffff));
}
get script_label(): number | undefined {
switch (this.type) {
case ObjectType.ScriptCollision:
case ObjectType.ForestConsole:
case ObjectType.TalkLinkToSupport:
return this.data.get_u32(52);
case ObjectType.RicoMessagePod:
return this.data.get_u32(56);
default:
return undefined;
}
}
get script_label_2(): number | undefined {
switch (this.type) {
case ObjectType.RicoMessagePod:
return this.data.get_u32(60);
default:
return undefined;
}
}
constructor(area_id: number, data: ArrayBufferBlock) {
assert(
data.size === OBJECT_BYTE_SIZE,
() => `Data size should be ${OBJECT_BYTE_SIZE} but was ${data.size}.`,
);
this.area_id = area_id;
this.data = data;
}
static create(type: ObjectType, area_id: number): QuestObject {
const obj = new QuestObject(
area_id,
new ArrayBufferBlock(OBJECT_BYTE_SIZE, Endianness.Little),
);
obj.type = type;
// Set area_id after type, because you might want to overwrite the area_id that type has
// determined.
obj.area_id = area_id;
return obj;
}
}
export type QuestEvent = DatEvent;
export function entity_type_to_string(type: EntityType): string {
return (NpcType as any)[type] ?? (ObjectType as any)[type];
}
export function is_npc_type(entity_type: EntityType): entity_type is NpcType {
return NpcType[entity_type] != undefined;
}
export function entity_data(type: EntityType): EntityTypeData {
return npc_data(type as NpcType) ?? object_data(type as ObjectType);
}

View File

@ -12,14 +12,14 @@ const PC_OBJECT_CODE_OFFSET = 920;
const BB_OBJECT_CODE_OFFSET = 4652;
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: ArrayBuffer;
readonly label_offsets: readonly number[];
readonly shop_items: readonly number[];
quest_id: number;
language: number;
quest_name: string;
short_description: string;
long_description: string;
object_code: ArrayBuffer;
readonly label_offsets: number[];
readonly shop_items: number[];
};
export function parse_bin(cursor: Cursor): { bin: BinFile; format: BinFormat } {

View File

@ -2,7 +2,7 @@ import { Endianness } from "../../block/Endianness";
import { prs_decompress } from "../../compression/prs/decompress";
import { BufferCursor } from "../../block/cursor/BufferCursor";
import { ResizableBlockCursor } from "../../block/cursor/ResizableBlockCursor";
import { DatFile, parse_dat, write_dat } from "./dat";
import { parse_dat, write_dat } from "./dat";
import { readFileSync } from "fs";
/**
@ -37,30 +37,15 @@ test("parse, modify and write DAT", () => {
const test_parsed = parse_dat(orig_dat);
orig_dat.seek_start(0);
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_obj_array = new Float32Array(test_parsed.objs[9].data);
test_obj_array[4] = 13;
test_obj_array[5] = 17;
test_obj_array[6] = 19;
const test_dat = new ResizableBlockCursor(write_dat(test_updated));
const test_dat = new ResizableBlockCursor(write_dat(test_parsed));
expect(test_dat.size).toBe(orig_dat.size);
let match = true;
while (orig_dat.bytes_left) {
if (orig_dat.position === 16 + 9 * 68 + 16) {
orig_dat.seek(12);
@ -68,11 +53,17 @@ test("parse, modify and write DAT", () => {
expect(test_dat.f32()).toBe(13);
expect(test_dat.f32()).toBe(17);
expect(test_dat.f32()).toBe(19);
} else if (test_dat.u8() !== orig_dat.u8()) {
match = false;
break;
}
}
} else {
const test_byte = test_dat.u8();
const orig_byte = orig_dat.u8();
expect(match).toBe(true);
if (test_byte !== orig_byte) {
throw new Error(
`Byte ${
test_dat.position - 1
} didn't match, expected ${orig_byte}, got ${test_byte}.`,
);
}
}
}
});

View File

@ -6,52 +6,33 @@ import { ResizableBlock } from "../../block/ResizableBlock";
import { WritableCursor } from "../../block/cursor/WritableCursor";
import { assert } from "../../../util";
import { LogManager } from "../../../Logger";
import { Vec3 } from "../../vector";
import { ArrayBufferCursor } from "../../block/cursor/ArrayBufferCursor";
const logger = LogManager.get("core/data_formats/parsing/quest/dat");
const OBJECT_SIZE = 68;
const NPC_SIZE = 72;
export const OBJECT_BYTE_SIZE = 68;
export const NPC_BYTE_SIZE = 72;
export type DatFile = {
readonly objs: readonly DatObject[];
readonly npcs: readonly DatNpc[];
readonly events: readonly DatEvent[];
readonly unknowns: readonly DatUnknown[];
readonly objs: DatEntity[];
readonly npcs: DatEntity[];
readonly events: DatEvent[];
readonly unknowns: DatUnknown[];
};
export type DatEntity = {
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 & {
readonly id: number;
readonly group_id: number;
readonly properties: readonly number[];
};
export type DatNpc = DatEntity & {
readonly wave: number;
readonly wave2: number;
readonly scale: Vec3;
readonly npc_id: number;
readonly script_label: number;
readonly roaming: number;
area_id: number;
readonly data: ArrayBuffer;
};
export type DatEvent = {
readonly id: number;
readonly section_id: number;
readonly wave: number;
readonly delay: number;
readonly actions: readonly DatEventAction[];
readonly area_id: number;
readonly unknown: number;
id: number;
section_id: number;
wave: number;
delay: number;
readonly actions: DatEventAction[];
area_id: number;
unknown: number;
};
export enum DatEventActionType {
@ -97,8 +78,8 @@ export type DatUnknown = {
};
export function parse_dat(cursor: Cursor): DatFile {
const objs: DatObject[] = [];
const npcs: DatNpc[] = [];
const objs: DatEntity[] = [];
const npcs: DatEntity[] = [];
const events: DatEvent[] = [];
const unknowns: DatUnknown[] = [];
@ -122,9 +103,9 @@ export function parse_dat(cursor: Cursor): DatFile {
const entities_cursor = cursor.take(entities_size);
if (entity_type === 1) {
parse_objects(entities_cursor, area_id, objs);
parse_entities(entities_cursor, area_id, objs, OBJECT_BYTE_SIZE);
} else if (entity_type === 2) {
parse_npcs(entities_cursor, area_id, npcs);
parse_entities(entities_cursor, area_id, npcs, NPC_BYTE_SIZE);
} else if (entity_type === 3) {
parse_events(entities_cursor, area_id, events);
} else {
@ -151,16 +132,16 @@ export function parse_dat(cursor: Cursor): DatFile {
export function write_dat({ objs, npcs, events, unknowns }: DatFile): ResizableBlock {
const block = new ResizableBlock(
objs.length * (16 + OBJECT_SIZE) +
npcs.length * (16 + NPC_SIZE) +
objs.length * (16 + OBJECT_BYTE_SIZE) +
npcs.length * (16 + NPC_BYTE_SIZE) +
unknowns.reduce((a, b) => a + b.total_size, 0),
Endianness.Little,
);
const cursor = new ResizableBlockCursor(block);
write_objects(cursor, objs);
write_entities(cursor, objs, 1, OBJECT_BYTE_SIZE);
write_npcs(cursor, npcs);
write_entities(cursor, npcs, 2, NPC_BYTE_SIZE);
write_events(cursor, events);
@ -181,78 +162,18 @@ export function write_dat({ objs, npcs, events, unknowns }: DatFile): ResizableB
return block;
}
function parse_objects(cursor: Cursor, area_id: number, objs: DatObject[]): void {
const object_count = Math.floor(cursor.size / OBJECT_SIZE);
function parse_entities(
cursor: Cursor,
area_id: number,
entities: DatEntity[],
entity_size: number,
): void {
const entity_count = Math.floor(cursor.size / entity_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,
for (let i = 0; i < entity_count; ++i) {
entities.push({
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 wave = cursor.u16();
const wave2 = cursor.u32();
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 unknown2 = cursor.u8_array(4);
npcs.push({
type_id,
section_id,
wave,
wave2,
position,
rotation: { x: rotation_x, y: rotation_y, z: rotation_z },
scale,
npc_id,
script_label,
roaming,
area_id,
unknown: [unknown1, unknown2],
data: cursor.array_buffer(entity_size),
});
}
}
@ -375,112 +296,43 @@ function parse_event_actions(cursor: Cursor): DatEventAction[] {
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)
function write_entities(
cursor: WritableCursor,
entities: readonly DatEntity[],
entity_type: number,
entity_size: number,
): void {
const grouped_entities = groupBy(entities, entity => entity.area_id);
const entity_area_ids = Object.keys(grouped_entities)
.map(key => parseInt(key, 10))
.sort((a, b) => a - b);
for (const area_id of obj_area_ids) {
const area_objs = grouped_objs[area_id];
const entities_size = area_objs.length * OBJECT_SIZE;
cursor.write_u32(1); // Entity type
for (const area_id of entity_area_ids) {
const area_entities = grouped_entities[area_id];
const entities_size = area_entities.length * entity_size;
cursor.write_u32(entity_type);
cursor.write_u32(entities_size + 16);
cursor.write_u32(area_id);
cursor.write_u32(entities_size);
const start_pos = cursor.position;
for (const obj of area_objs) {
for (const entity of area_entities) {
assert(
obj.unknown.length === 2,
() => `unknown should be of length 2, was ${obj.unknown.length}`,
entity.data.byteLength === entity_size,
() =>
`Malformed entity in area ${area_id}, data array was of length ${entity.data.byteLength} instead of expected ${entity_size}.`,
);
cursor.write_u16(obj.type_id);
assert(
obj.unknown[0].length === 6,
() => `unknown[0] should be of length 6, was ${obj.unknown[0].length}`,
);
cursor.write_u8_array(obj.unknown[0]);
cursor.write_u16(obj.id);
cursor.write_u16(obj.group_id);
cursor.write_u16(obj.section_id);
assert(
obj.unknown[1].length === 2,
() => `unknown[1] should be of length 2, was ${obj.unknown[1].length}`,
);
cursor.write_u8_array(obj.unknown[1]);
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));
assert(
obj.properties.length === 7,
() => `properties should be of length 7, was ${obj.properties.length}`,
);
cursor.write_f32(obj.properties[0]);
cursor.write_f32(obj.properties[1]);
cursor.write_f32(obj.properties[2]);
cursor.write_u32(obj.properties[3]);
cursor.write_u32(obj.properties[4]);
cursor.write_u32(obj.properties[5]);
cursor.write_u32(obj.properties[6]);
cursor.write_cursor(new ArrayBufferCursor(entity.data, cursor.endianness));
}
}
}
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))
.sort((a, b) => a - b);
for (const area_id of npc_area_ids) {
const area_npcs = grouped_npcs[area_id];
const entities_size = area_npcs.length * NPC_SIZE;
cursor.write_u32(2); // Entity type
cursor.write_u32(entities_size + 16);
cursor.write_u32(area_id);
cursor.write_u32(entities_size);
for (const npc of area_npcs) {
assert(
npc.unknown.length === 2,
() => `unknown should be of length 2, was ${npc.unknown.length}`,
);
cursor.write_u16(npc.type_id);
assert(
npc.unknown[0].length === 10,
() => `unknown[0] should be of length 10, was ${npc.unknown[0].length}`,
cursor.position === start_pos + entities_size,
() =>
`Wrote ${
cursor.position - start_pos
} bytes of entity data instead of expected ${entities_size} bytes for area ${area_id}.`,
);
cursor.write_u8_array(npc.unknown[0]);
cursor.write_u16(npc.section_id);
cursor.write_u16(npc.wave);
cursor.write_u32(npc.wave2);
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_f32(npc.npc_id);
cursor.write_f32(npc.script_label);
cursor.write_u32(npc.roaming);
assert(
npc.unknown[1].length === 4,
() => `unknown[1] should be of length 4, was ${npc.unknown[1].length}`,
);
cursor.write_u8_array(npc.unknown[1]);
}
}
}

View File

@ -1,72 +0,0 @@
import { Vec3 } from "../../vector";
import { npc_data, NpcType, NpcTypeData } from "./npc_types";
import { object_data, ObjectType, ObjectTypeData } from "./object_types";
import { DatEvent } from "./dat";
export type QuestNpc = {
readonly type: NpcType;
readonly area_id: number;
readonly section_id: number;
readonly wave: number;
readonly pso_wave2: number;
/**
* Section-relative position
*/
readonly position: Vec3;
readonly rotation: Vec3;
/**
* Seemingly 3 floats, not sure what they represent.
* The y component is used to help determine what the NpcType is.
*/
readonly scale: Vec3;
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: readonly number[][];
readonly pso_type_id: number;
readonly npc_id: number;
/**
* Only seems to be valid for non-enemies.
*/
readonly script_label: number;
readonly pso_roaming: number;
};
export type QuestObject = {
readonly type: ObjectType;
readonly id: number;
readonly group_id: number;
readonly area_id: number;
readonly section_id: number;
/**
* Section-relative position
*/
readonly position: Vec3;
readonly rotation: Vec3;
/**
* Properties that differ per object type.
*/
readonly properties: Map<string, number>;
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: readonly number[][];
};
export type QuestEvent = DatEvent;
export type EntityTypeData = NpcTypeData | ObjectTypeData;
export type EntityType = NpcType | ObjectType;
export function entity_type_to_string(type: EntityType): string {
return (NpcType as any)[type] ?? (ObjectType as any)[type];
}
export function is_npc_type(entity_type: EntityType): entity_type is NpcType {
return NpcType[entity_type] != undefined;
}
export function entity_data(type: EntityType): EntityTypeData {
return npc_data(type as NpcType) ?? object_data(type as ObjectType);
}

View File

@ -0,0 +1,281 @@
import { NpcType } from "./npc_types";
// TODO: detect Mothmant, St. Rappy, Hallo Rappy, Egg Rappy, Death Gunner, Bulk and Recon.
export function get_npc_type(
episode: number,
type_id: number,
regular: boolean,
skin: number,
area_id: number,
): NpcType {
switch (`${type_id}, ${skin % 3}, ${episode}`) {
case `${0x044}, 0, 1`:
return NpcType.Booma;
case `${0x044}, 1, 1`:
return NpcType.Gobooma;
case `${0x044}, 2, 1`:
return NpcType.Gigobooma;
case `${0x063}, 0, 1`:
return NpcType.EvilShark;
case `${0x063}, 1, 1`:
return NpcType.PalShark;
case `${0x063}, 2, 1`:
return NpcType.GuilShark;
case `${0x0a6}, 0, 1`:
return NpcType.Dimenian;
case `${0x0a6}, 0, 2`:
return NpcType.Dimenian2;
case `${0x0a6}, 1, 1`:
return NpcType.LaDimenian;
case `${0x0a6}, 1, 2`:
return NpcType.LaDimenian2;
case `${0x0a6}, 2, 1`:
return NpcType.SoDimenian;
case `${0x0a6}, 2, 2`:
return NpcType.SoDimenian2;
case `${0x0d6}, 0, 2`:
return NpcType.Mericarol;
case `${0x0d6}, 1, 2`:
return NpcType.Mericus;
case `${0x0d6}, 2, 2`:
return NpcType.Merikle;
case `${0x115}, 0, 4`:
return NpcType.Boota;
case `${0x115}, 1, 4`:
return NpcType.ZeBoota;
case `${0x115}, 2, 4`:
return NpcType.BaBoota;
case `${0x117}, 0, 4`:
return NpcType.Goran;
case `${0x117}, 1, 4`:
return NpcType.PyroGoran;
case `${0x117}, 2, 4`:
return NpcType.GoranDetonator;
}
switch (`${type_id}, ${skin % 2}, ${episode}`) {
case `${0x040}, 0, 1`:
return NpcType.Hildebear;
case `${0x040}, 0, 2`:
return NpcType.Hildebear2;
case `${0x040}, 1, 1`:
return NpcType.Hildeblue;
case `${0x040}, 1, 2`:
return NpcType.Hildeblue2;
case `${0x041}, 0, 1`:
return NpcType.RagRappy;
case `${0x041}, 0, 2`:
return NpcType.RagRappy2;
case `${0x041}, 0, 4`:
return NpcType.SandRappy;
case `${0x041}, 1, 1`:
return NpcType.AlRappy;
case `${0x041}, 1, 2`:
return NpcType.LoveRappy;
case `${0x041}, 1, 4`:
return NpcType.DelRappy;
case `${0x080}, 0, 1`:
return NpcType.Dubchic;
case `${0x080}, 0, 2`:
return NpcType.Dubchic2;
case `${0x080}, 1, 1`:
return NpcType.Gilchic;
case `${0x080}, 1, 2`:
return NpcType.Gilchic2;
case `${0x0d4}, 0, 2`:
return NpcType.SinowBerill;
case `${0x0d4}, 1, 2`:
return NpcType.SinowSpigell;
case `${0x0d5}, 0, 2`:
return NpcType.Merillia;
case `${0x0d5}, 1, 2`:
return NpcType.Meriltas;
case `${0x0d7}, 0, 2`:
return NpcType.UlGibbon;
case `${0x0d7}, 1, 2`:
return NpcType.ZolGibbon;
case `${0x0dd}, 0, 2`:
return NpcType.Dolmolm;
case `${0x0dd}, 1, 2`:
return NpcType.Dolmdarl;
case `${0x0e0}, 0, 2`:
return area_id > 15 ? NpcType.Epsilon : NpcType.SinowZoa;
case `${0x0e0}, 1, 2`:
return area_id > 15 ? NpcType.Epsilon : NpcType.SinowZele;
case `${0x112}, 0, 4`:
return NpcType.MerissaA;
case `${0x112}, 1, 4`:
return NpcType.MerissaAA;
case `${0x114}, 0, 4`:
return NpcType.Zu;
case `${0x114}, 1, 4`:
return NpcType.Pazuzu;
case `${0x116}, 0, 4`:
return NpcType.Dorphon;
case `${0x116}, 1, 4`:
return NpcType.DorphonEclair;
case `${0x119}, 0, 4`:
return regular ? NpcType.SaintMilion : NpcType.Kondrieu;
case `${0x119}, 1, 4`:
return regular ? NpcType.Shambertin : NpcType.Kondrieu;
}
switch (`${type_id}, ${episode}`) {
case `${0x042}, 1`:
return NpcType.Monest;
case `${0x042}, 2`:
return NpcType.Monest2;
case `${0x043}, 1`:
return regular ? NpcType.SavageWolf : NpcType.BarbarousWolf;
case `${0x043}, 2`:
return regular ? NpcType.SavageWolf2 : NpcType.BarbarousWolf2;
case `${0x060}, 1`:
return NpcType.GrassAssassin;
case `${0x060}, 2`:
return NpcType.GrassAssassin2;
case `${0x061}, 1`:
return area_id > 15 ? NpcType.DelLily : regular ? NpcType.PoisonLily : NpcType.NarLily;
case `${0x061}, 2`:
return area_id > 15
? NpcType.DelLily
: regular
? NpcType.PoisonLily2
: NpcType.NarLily2;
case `${0x062}, 1`:
return NpcType.NanoDragon;
case `${0x064}, 1`:
return regular ? NpcType.PofuillySlime : NpcType.PouillySlime;
case `${0x065}, 1`:
return NpcType.PanArms;
case `${0x065}, 2`:
return NpcType.PanArms2;
case `${0x081}, 1`:
return NpcType.Garanz;
case `${0x081}, 2`:
return NpcType.Garanz2;
case `${0x082}, 1`:
return regular ? NpcType.SinowBeat : NpcType.SinowGold;
case `${0x083}, 1`:
return NpcType.Canadine;
case `${0x084}, 1`:
return NpcType.Canane;
case `${0x085}, 1`:
return NpcType.Dubswitch;
case `${0x085}, 2`:
return NpcType.Dubswitch2;
case `${0x0a0}, 1`:
return NpcType.Delsaber;
case `${0x0a0}, 2`:
return NpcType.Delsaber2;
case `${0x0a1}, 1`:
return NpcType.ChaosSorcerer;
case `${0x0a1}, 2`:
return NpcType.ChaosSorcerer2;
case `${0x0a2}, 1`:
return NpcType.DarkGunner;
case `${0x0a4}, 1`:
return NpcType.ChaosBringer;
case `${0x0a5}, 1`:
return NpcType.DarkBelra;
case `${0x0a5}, 2`:
return NpcType.DarkBelra2;
case `${0x0a7}, 1`:
return NpcType.Bulclaw;
case `${0x0a8}, 1`:
return NpcType.Claw;
case `${0x0c0}, 1`:
return NpcType.Dragon;
case `${0x0c0}, 2`:
return NpcType.GalGryphon;
case `${0x0c1}, 1`:
return NpcType.DeRolLe;
case `${0x0c2}, 1`:
return NpcType.VolOptPart1;
case `${0x0c5}, 1`:
return NpcType.VolOptPart2;
case `${0x0c8}, 1`:
return NpcType.DarkFalz;
case `${0x0ca}, 2`:
return NpcType.OlgaFlow;
case `${0x0cb}, 2`:
return NpcType.BarbaRay;
case `${0x0cc}, 2`:
return NpcType.GolDragon;
case `${0x0d8}, 2`:
return NpcType.Gibbles;
case `${0x0d9}, 2`:
return NpcType.Gee;
case `${0x0da}, 2`:
return NpcType.GiGue;
case `${0x0db}, 2`:
return NpcType.Deldepth;
case `${0x0dc}, 2`:
return NpcType.Delbiter;
case `${0x0de}, 2`:
return NpcType.Morfos;
case `${0x0df}, 2`:
return NpcType.Recobox;
case `${0x0e1}, 2`:
return NpcType.IllGill;
case `${0x110}, 4`:
return NpcType.Astark;
case `${0x111}, 4`:
return regular ? NpcType.SatelliteLizard : NpcType.Yowie;
case `${0x113}, 4`:
return NpcType.Girtablulu;
}
switch (type_id) {
case 0x004:
return NpcType.FemaleFat;
case 0x005:
return NpcType.FemaleMacho;
case 0x007:
return NpcType.FemaleTall;
case 0x00a:
return NpcType.MaleDwarf;
case 0x00b:
return NpcType.MaleFat;
case 0x00c:
return NpcType.MaleMacho;
case 0x00d:
return NpcType.MaleOld;
case 0x019:
return NpcType.BlueSoldier;
case 0x01a:
return NpcType.RedSoldier;
case 0x01b:
return NpcType.Principal;
case 0x01c:
return NpcType.Tekker;
case 0x01d:
return NpcType.GuildLady;
case 0x01e:
return NpcType.Scientist;
case 0x01f:
return NpcType.Nurse;
case 0x020:
return NpcType.Irene;
case 0x0f1:
return NpcType.ItemShop;
case 0x0fe:
return NpcType.Nurse2;
}
return NpcType.Unknown;
}

View File

@ -1,4 +1,4 @@
import { InstructionSegment, Segment, SegmentType } from "../../asm/instructions";
import { InstructionSegment, SegmentType } from "../../asm/instructions";
import { OP_SET_EPISODE } from "../../asm/opcodes";
import { prs_compress } from "../../compression/prs/compress";
import { prs_decompress } from "../../compression/prs/decompress";
@ -7,41 +7,20 @@ import { Cursor } from "../../block/cursor/Cursor";
import { ResizableBlockCursor } from "../../block/cursor/ResizableBlockCursor";
import { Endianness } from "../../block/Endianness";
import { parse_bin, write_bin } from "./bin";
import { DatNpc, DatObject, DatUnknown, parse_dat, write_dat } from "./dat";
import { QuestEvent, QuestNpc, QuestObject } from "./entities";
import { DatEntity, parse_dat, write_dat } from "./dat";
import { Quest, QuestNpc, QuestObject } from "./Quest";
import { Episode } from "./Episode";
import { object_data, ObjectType, pso_id_to_object_type } from "./object_types";
import { parse_qst, QstContainedFile, write_qst } from "./qst";
import { npc_data, NpcType } from "./npc_types";
import { reinterpret_f32_as_i32, reinterpret_i32_as_f32 } from "../../../primitive_conversion";
import { LogManager } from "../../../Logger";
import { 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";
import { ArrayBufferBlock } from "../../block/ArrayBufferBlock";
const logger = LogManager.get("core/data_formats/parsing/quest");
export type Quest = {
readonly id: number;
readonly language: number;
readonly name: string;
readonly short_description: string;
readonly long_description: string;
readonly episode: Episode;
readonly objects: readonly QuestObject[];
readonly npcs: readonly QuestNpc[];
readonly events: readonly QuestEvent[];
/**
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
*/
readonly dat_unknowns: readonly DatUnknown[];
readonly object_code: readonly Segment[];
readonly shop_items: readonly number[];
readonly map_designations: Map<number, number>;
};
export function parse_bin_dat_to_quest(
bin_cursor: Cursor,
dat_cursor: Cursor,
@ -54,6 +33,8 @@ export function parse_bin_dat_to_quest(
const dat_decompressed = prs_decompress(dat_cursor);
const dat = parse_dat(dat_decompressed);
const objects = parse_obj_data(dat.objs);
// Initialize NPCs with random episode and correct it later.
const npcs = parse_npc_data(Episode.I, dat.npcs);
// Extract episode and map designations from object code.
let episode = Episode.I;
@ -62,7 +43,7 @@ export function parse_bin_dat_to_quest(
const object_code = parse_object_code(
bin.object_code,
bin.label_offsets,
extract_script_entry_points(objects, dat.npcs),
extract_script_entry_points(objects, npcs),
lenient,
format,
);
@ -83,6 +64,11 @@ export function parse_bin_dat_to_quest(
if (label_0_segment) {
episode = get_episode(label_0_segment);
for (const npc of npcs) {
npc.episode = episode;
}
map_designations = get_map_designations(instruction_segments, label_0_segment);
} else {
logger.warn(`No instruction for label 0 found.`);
@ -91,21 +77,21 @@ export function parse_bin_dat_to_quest(
logger.warn("File contains no instruction labels.");
}
return {
id: bin.quest_id,
language: bin.language,
name: bin.quest_name,
short_description: bin.short_description,
long_description: bin.long_description,
return new Quest(
bin.quest_id,
bin.language,
bin.quest_name,
bin.short_description,
bin.long_description,
episode,
objects,
npcs: parse_npc_data(episode, dat.npcs),
events: dat.events,
dat_unknowns: dat.unknowns,
npcs,
dat.events,
dat.unknowns,
object_code,
shop_items: bin.shop_items,
bin.shop_items,
map_designations,
};
);
}
export function parse_qst_to_quest(
@ -235,18 +221,18 @@ function get_episode(func_0_segment: InstructionSegment): Episode {
function extract_script_entry_points(
objects: readonly QuestObject[],
npcs: readonly DatNpc[],
npcs: readonly QuestNpc[],
): number[] {
const entry_points = new Set([0]);
for (const obj of objects) {
const entry_point = obj.properties.get("script_label");
const entry_point = obj.script_label;
if (entry_point != undefined) {
entry_points.add(entry_point);
}
const entry_point_2 = obj.properties.get("script_label_2");
const entry_point_2 = obj.script_label_2;
if (entry_point_2 != undefined) {
entry_points.add(entry_point_2);
@ -254,403 +240,43 @@ function extract_script_entry_points(
}
for (const npc of npcs) {
entry_points.add(Math.round(npc.script_label));
entry_points.add(npc.script_label);
}
return [...entry_points];
}
function parse_obj_data(objs: readonly DatObject[]): QuestObject[] {
return objs.map(obj_data => {
const type = pso_id_to_object_type(obj_data.type_id);
return {
type,
id: obj_data.id,
group_id: obj_data.group_id,
area_id: obj_data.area_id,
section_id: obj_data.section_id,
position: obj_data.position,
rotation: obj_data.rotation,
properties: new Map(
obj_data.properties.map((value, index) => {
if (
index === 3 &&
(type === ObjectType.ScriptCollision ||
type === ObjectType.ForestConsole ||
type === ObjectType.TalkLinkToSupport)
) {
return ["script_label", value];
} else if (index === 4 && type === ObjectType.RicoMessagePod) {
return ["script_label", value];
} else if (index === 5 && type === ObjectType.RicoMessagePod) {
return ["script_label_2", value];
} else {
return [`property_${index}`, value];
}
}),
function parse_obj_data(objs: readonly DatEntity[]): QuestObject[] {
return objs.map(
obj_data =>
new QuestObject(
obj_data.area_id,
new ArrayBufferBlock(obj_data.data, Endianness.Little),
),
unknown: obj_data.unknown,
};
});
}
function parse_npc_data(episode: number, npcs: readonly DatNpc[]): QuestNpc[] {
return npcs.map(npc_data => {
return {
type: get_npc_type(episode, npc_data),
area_id: npc_data.area_id,
section_id: npc_data.section_id,
wave: npc_data.wave,
pso_wave2: npc_data.wave2,
position: npc_data.position,
rotation: npc_data.rotation,
scale: npc_data.scale,
unknown: npc_data.unknown,
pso_type_id: npc_data.type_id,
npc_id: npc_data.npc_id,
script_label: Math.round(npc_data.script_label),
pso_roaming: npc_data.roaming,
};
});
}
// TODO: detect Mothmant, St. Rappy, Hallo Rappy, Egg Rappy, Death Gunner, Bulk and Recon.
function get_npc_type(episode: number, { type_id, scale, roaming, area_id }: DatNpc): NpcType {
const regular = Math.abs(scale.y - 1) > 0.00001;
switch (`${type_id}, ${roaming % 3}, ${episode}`) {
case `${0x044}, 0, 1`:
return NpcType.Booma;
case `${0x044}, 1, 1`:
return NpcType.Gobooma;
case `${0x044}, 2, 1`:
return NpcType.Gigobooma;
case `${0x063}, 0, 1`:
return NpcType.EvilShark;
case `${0x063}, 1, 1`:
return NpcType.PalShark;
case `${0x063}, 2, 1`:
return NpcType.GuilShark;
case `${0x0a6}, 0, 1`:
return NpcType.Dimenian;
case `${0x0a6}, 0, 2`:
return NpcType.Dimenian2;
case `${0x0a6}, 1, 1`:
return NpcType.LaDimenian;
case `${0x0a6}, 1, 2`:
return NpcType.LaDimenian2;
case `${0x0a6}, 2, 1`:
return NpcType.SoDimenian;
case `${0x0a6}, 2, 2`:
return NpcType.SoDimenian2;
case `${0x0d6}, 0, 2`:
return NpcType.Mericarol;
case `${0x0d6}, 1, 2`:
return NpcType.Mericus;
case `${0x0d6}, 2, 2`:
return NpcType.Merikle;
case `${0x115}, 0, 4`:
return NpcType.Boota;
case `${0x115}, 1, 4`:
return NpcType.ZeBoota;
case `${0x115}, 2, 4`:
return NpcType.BaBoota;
case `${0x117}, 0, 4`:
return NpcType.Goran;
case `${0x117}, 1, 4`:
return NpcType.PyroGoran;
case `${0x117}, 2, 4`:
return NpcType.GoranDetonator;
}
switch (`${type_id}, ${roaming % 2}, ${episode}`) {
case `${0x040}, 0, 1`:
return NpcType.Hildebear;
case `${0x040}, 0, 2`:
return NpcType.Hildebear2;
case `${0x040}, 1, 1`:
return NpcType.Hildeblue;
case `${0x040}, 1, 2`:
return NpcType.Hildeblue2;
case `${0x041}, 0, 1`:
return NpcType.RagRappy;
case `${0x041}, 0, 2`:
return NpcType.RagRappy2;
case `${0x041}, 0, 4`:
return NpcType.SandRappy;
case `${0x041}, 1, 1`:
return NpcType.AlRappy;
case `${0x041}, 1, 2`:
return NpcType.LoveRappy;
case `${0x041}, 1, 4`:
return NpcType.DelRappy;
case `${0x080}, 0, 1`:
return NpcType.Dubchic;
case `${0x080}, 0, 2`:
return NpcType.Dubchic2;
case `${0x080}, 1, 1`:
return NpcType.Gilchic;
case `${0x080}, 1, 2`:
return NpcType.Gilchic2;
case `${0x0d4}, 0, 2`:
return NpcType.SinowBerill;
case `${0x0d4}, 1, 2`:
return NpcType.SinowSpigell;
case `${0x0d5}, 0, 2`:
return NpcType.Merillia;
case `${0x0d5}, 1, 2`:
return NpcType.Meriltas;
case `${0x0d7}, 0, 2`:
return NpcType.UlGibbon;
case `${0x0d7}, 1, 2`:
return NpcType.ZolGibbon;
case `${0x0dd}, 0, 2`:
return NpcType.Dolmolm;
case `${0x0dd}, 1, 2`:
return NpcType.Dolmdarl;
case `${0x0e0}, 0, 2`:
return area_id > 15 ? NpcType.Epsilon : NpcType.SinowZoa;
case `${0x0e0}, 1, 2`:
return area_id > 15 ? NpcType.Epsilon : NpcType.SinowZele;
case `${0x112}, 0, 4`:
return NpcType.MerissaA;
case `${0x112}, 1, 4`:
return NpcType.MerissaAA;
case `${0x114}, 0, 4`:
return NpcType.Zu;
case `${0x114}, 1, 4`:
return NpcType.Pazuzu;
case `${0x116}, 0, 4`:
return NpcType.Dorphon;
case `${0x116}, 1, 4`:
return NpcType.DorphonEclair;
case `${0x119}, 0, 4`:
return regular ? NpcType.SaintMilion : NpcType.Kondrieu;
case `${0x119}, 1, 4`:
return regular ? NpcType.Shambertin : NpcType.Kondrieu;
}
switch (`${type_id}, ${episode}`) {
case `${0x042}, 1`:
return NpcType.Monest;
case `${0x042}, 2`:
return NpcType.Monest2;
case `${0x043}, 1`:
return regular ? NpcType.SavageWolf : NpcType.BarbarousWolf;
case `${0x043}, 2`:
return regular ? NpcType.SavageWolf2 : NpcType.BarbarousWolf2;
case `${0x060}, 1`:
return NpcType.GrassAssassin;
case `${0x060}, 2`:
return NpcType.GrassAssassin2;
case `${0x061}, 1`:
return area_id > 15 ? NpcType.DelLily : regular ? NpcType.PoisonLily : NpcType.NarLily;
case `${0x061}, 2`:
return area_id > 15
? NpcType.DelLily
: regular
? NpcType.PoisonLily2
: NpcType.NarLily2;
case `${0x062}, 1`:
return NpcType.NanoDragon;
case `${0x064}, 1`:
return regular ? NpcType.PofuillySlime : NpcType.PouillySlime;
case `${0x065}, 1`:
return NpcType.PanArms;
case `${0x065}, 2`:
return NpcType.PanArms2;
case `${0x081}, 1`:
return NpcType.Garanz;
case `${0x081}, 2`:
return NpcType.Garanz2;
case `${0x082}, 1`:
return regular ? NpcType.SinowBeat : NpcType.SinowGold;
case `${0x083}, 1`:
return NpcType.Canadine;
case `${0x084}, 1`:
return NpcType.Canane;
case `${0x085}, 1`:
return NpcType.Dubswitch;
case `${0x085}, 2`:
return NpcType.Dubswitch2;
case `${0x0a0}, 1`:
return NpcType.Delsaber;
case `${0x0a0}, 2`:
return NpcType.Delsaber2;
case `${0x0a1}, 1`:
return NpcType.ChaosSorcerer;
case `${0x0a1}, 2`:
return NpcType.ChaosSorcerer2;
case `${0x0a2}, 1`:
return NpcType.DarkGunner;
case `${0x0a4}, 1`:
return NpcType.ChaosBringer;
case `${0x0a5}, 1`:
return NpcType.DarkBelra;
case `${0x0a5}, 2`:
return NpcType.DarkBelra2;
case `${0x0a7}, 1`:
return NpcType.Bulclaw;
case `${0x0a8}, 1`:
return NpcType.Claw;
case `${0x0c0}, 1`:
return NpcType.Dragon;
case `${0x0c0}, 2`:
return NpcType.GalGryphon;
case `${0x0c1}, 1`:
return NpcType.DeRolLe;
case `${0x0c2}, 1`:
return NpcType.VolOptPart1;
case `${0x0c5}, 1`:
return NpcType.VolOptPart2;
case `${0x0c8}, 1`:
return NpcType.DarkFalz;
case `${0x0ca}, 2`:
return NpcType.OlgaFlow;
case `${0x0cb}, 2`:
return NpcType.BarbaRay;
case `${0x0cc}, 2`:
return NpcType.GolDragon;
case `${0x0d8}, 2`:
return NpcType.Gibbles;
case `${0x0d9}, 2`:
return NpcType.Gee;
case `${0x0da}, 2`:
return NpcType.GiGue;
case `${0x0db}, 2`:
return NpcType.Deldepth;
case `${0x0dc}, 2`:
return NpcType.Delbiter;
case `${0x0de}, 2`:
return NpcType.Morfos;
case `${0x0df}, 2`:
return NpcType.Recobox;
case `${0x0e1}, 2`:
return NpcType.IllGill;
case `${0x110}, 4`:
return NpcType.Astark;
case `${0x111}, 4`:
return regular ? NpcType.SatelliteLizard : NpcType.Yowie;
case `${0x113}, 4`:
return NpcType.Girtablulu;
}
switch (type_id) {
case 0x004:
return NpcType.FemaleFat;
case 0x005:
return NpcType.FemaleMacho;
case 0x007:
return NpcType.FemaleTall;
case 0x00a:
return NpcType.MaleDwarf;
case 0x00b:
return NpcType.MaleFat;
case 0x00c:
return NpcType.MaleMacho;
case 0x00d:
return NpcType.MaleOld;
case 0x019:
return NpcType.BlueSoldier;
case 0x01a:
return NpcType.RedSoldier;
case 0x01b:
return NpcType.Principal;
case 0x01c:
return NpcType.Tekker;
case 0x01d:
return NpcType.GuildLady;
case 0x01e:
return NpcType.Scientist;
case 0x01f:
return NpcType.Nurse;
case 0x020:
return NpcType.Irene;
case 0x0f1:
return NpcType.ItemShop;
case 0x0fe:
return NpcType.Nurse2;
}
return NpcType.Unknown;
}
function objects_to_dat_data(objects: readonly QuestObject[]): DatObject[] {
return objects.map(object => {
const props = [...object.properties.values()];
const props_target_len = 7;
// Truncate or pad property list if it is not the correct length.
if (props.length > props_target_len) {
logger.warn(
`Object #${object.id} has too many properties. Truncating property list to length of ${props_target_len}.`,
);
props.splice(props_target_len);
} else if (props.length < props_target_len) {
const to_add = props_target_len - props.length;
for (let i = 0; i < to_add; i++) {
props.push(0);
}
}
}
return {
type_id: object_data(object.type).pso_id!,
id: object.id,
group_id: object.group_id,
section_id: object.section_id,
position: object.position,
rotation: object.rotation,
properties: props,
function parse_npc_data(episode: number, npcs: readonly DatEntity[]): QuestNpc[] {
return npcs.map(
npc_data =>
new QuestNpc(
episode,
npc_data.area_id,
new ArrayBufferBlock(npc_data.data, Endianness.Little),
),
);
}
function objects_to_dat_data(objects: readonly QuestObject[]): DatEntity[] {
return objects.map(object => ({
area_id: object.area_id,
unknown: object.unknown,
};
});
data: object.data.backing_buffer,
}));
}
function npcs_to_dat_data(npcs: readonly QuestNpc[]): DatNpc[] {
return npcs.map(npc => {
const type_data = npc_data(npc.type);
const type_id =
type_data.pso_type_id == undefined ? npc.pso_type_id : type_data.pso_type_id;
const roaming =
type_data.pso_roaming == undefined ? npc.pso_roaming : type_data.pso_roaming;
const regular = type_data.pso_regular == undefined ? true : type_data.pso_regular;
const scale_y = reinterpret_i32_as_f32(
(reinterpret_f32_as_i32(npc.scale.y) & ~0x800000) | (regular ? 0 : 0x800000),
);
const scale = { x: npc.scale.x, y: scale_y, z: npc.scale.z };
return {
type_id,
wave: npc.wave,
wave2: npc.pso_wave2,
section_id: npc.section_id,
position: npc.position,
rotation: npc.rotation,
scale,
npc_id: npc.npc_id,
script_label: npc.script_label,
roaming,
function npcs_to_dat_data(npcs: readonly QuestNpc[]): DatEntity[] {
return npcs.map(npc => ({
area_id: npc.area_id,
unknown: npc.unknown,
};
});
data: npc.data.backing_buffer,
}));
}

View File

@ -210,18 +210,18 @@ export type NpcTypeData = {
/**
* Type ID used by the game.
*/
readonly pso_type_id?: number;
readonly type_id?: number;
/**
* Roaming value used by the game.
* Skin value used by the game.
*/
readonly pso_roaming?: number;
readonly skin?: number;
/**
* Boolean specifying whether an NPC is the regular or special variant. The game uses a single
* bit in the y component of the NPC's scale vector for this value.
* Sometimes signifies a variant (e.g. Barbarous Wolf), sometimes a rare variant (e.g. Pouilly
* Slime).
*/
readonly pso_regular?: boolean;
readonly regular?: boolean;
};
export const NPC_TYPES: NpcType[] = [];
@ -258,9 +258,9 @@ function define_npc_type_data(
enemy: boolean,
rare_type: NpcType | undefined,
area_ids: number[],
pso_type_id: number | undefined,
pso_roaming: number | undefined,
pso_regular: boolean | undefined,
type_id: number | undefined,
skin: number | undefined,
regular: boolean | undefined,
): void {
NPC_TYPES.push(npc_type);
@ -276,9 +276,9 @@ function define_npc_type_data(
enemy,
rare_type,
area_ids,
pso_type_id,
pso_roaming,
pso_regular,
type_id,
skin,
regular,
});
if (episode) {

View File

@ -291,7 +291,7 @@ export type ObjectTypeData = {
* This array can be indexed with an {@link Episode} value.
*/
readonly area_ids: number[][];
readonly pso_id?: number;
readonly type_id?: number;
};
export const OBJECT_TYPES: ObjectType[] = [];
@ -300,8 +300,8 @@ export function object_data(type: ObjectType): ObjectTypeData {
return OBJECT_TYPE_DATA[type];
}
export function pso_id_to_object_type(psoId: number): ObjectType {
switch (psoId) {
export function id_to_object_type(id: number): ObjectType {
switch (id) {
default:
return ObjectType.Unknown;
@ -870,7 +870,7 @@ const OBJECT_TYPE_DATA: ObjectTypeData[] = [];
function define_object_type_data(
object_type: ObjectType,
pso_id: number | undefined,
type_id: number | undefined,
name: string,
area_ids: [Episode, number[]][],
): void {
@ -885,7 +885,7 @@ function define_object_type_data(
OBJECT_TYPE_DATA[object_type] = Object.freeze({
name,
area_ids: area_ids_per_episode,
pso_id,
type_id,
});
}

View File

@ -130,3 +130,7 @@ export function number_to_hex_string(num: number, min_len: number = 8): string {
export function browser_supports_webassembly(): boolean {
return typeof window === "object" && typeof window.WebAssembly === "object";
}
export function is_promise(value: unknown): value is Promise<any> {
return value && typeof value === "object" && "then" in value && "finally" in value;
}

View File

@ -1,8 +1,8 @@
import { Action } from "../../core/undo/Action";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
import { QuestModel } from "../model/QuestModel";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
export class CreateEntityAction implements Action {
readonly description: string;

View File

@ -1,8 +1,8 @@
import { Action } from "../../core/undo/Action";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { QuestModel } from "../model/QuestModel";
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
export class RemoveEntityAction implements Action {
readonly description: string;

View File

@ -1,8 +1,8 @@
import { Action } from "../../core/undo/Action";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
import { Euler } from "three";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
export class RotateEntityAction implements Action {
readonly description: string;

View File

@ -1,9 +1,9 @@
import { Action } from "../../core/undo/Action";
import { QuestEntityModel } from "../model/QuestEntityModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
import { SectionModel } from "../model/SectionModel";
import { Vector3 } from "three";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
export class TranslateEntityAction implements Action {
readonly description: string;

View File

@ -2,11 +2,11 @@ import { with_disposer } from "../../../test/src/core/observables/disposable_hel
import { GuiStore } from "../../core/stores/GuiStore";
import { create_area_store } from "../../../test/src/quest_editor/stores/store_creation";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { DebugController } from "./DebugController";
import { LogStore } from "../stores/LogStore";
import { create_new_quest } from "../stores/quest_creation";
import { next_animation_frame } from "../../../test/src/utils";
import { load_default_quest_model, next_animation_frame } from "../../../test/src/utils";
import { disassemble } from "../scripting/disassembly";
import { assemble } from "../scripting/assembly";
test("Some widgets should only be enabled when a quest is loaded.", async () =>
with_disposer(async disposer => {
@ -21,7 +21,7 @@ test("Some widgets should only be enabled when a quest is loaded.", async () =>
expect(ctrl.can_debug.val).toBe(false);
expect(ctrl.can_step.val).toBe(false);
await quest_editor_store.set_current_quest(create_new_quest(area_store, Episode.I));
await quest_editor_store.set_current_quest(load_default_quest_model(area_store));
expect(ctrl.can_debug.val).toBe(true);
expect(ctrl.can_step.val).toBe(false);
@ -37,26 +37,41 @@ test("Debugging controls should be enabled and disabled at the right times.", as
);
const ctrl = disposer.add(new DebugController(gui_store, quest_editor_store, log_store));
await quest_editor_store.set_current_quest(create_new_quest(area_store, Episode.I));
const quest = load_default_quest_model(area_store);
// Disassemble and reassemble the IR to ensure we have source locations in the final IR.
quest.object_code.splice(
0,
Infinity,
...assemble(disassemble(quest.object_code)).object_code,
);
await quest_editor_store.set_current_quest(quest);
// Before starting we can't step or stop.
expect(ctrl.can_step.val).toBe(false);
expect(ctrl.can_stop.val).toBe(false);
ctrl.debug();
await next_animation_frame();
// When all threads have yielded, all we can do is stop.
expect(ctrl.can_step.val).toBe(false);
expect(ctrl.can_stop.val).toBe(true);
ctrl.stop();
// After stopping we can't step or stop anymore.
expect(ctrl.can_step.val).toBe(false);
expect(ctrl.can_stop.val).toBe(false);
quest_editor_store.quest_runner.set_breakpoint(5);
// After hitting a breakpoint, we can step and stop.
expect(quest_editor_store.quest_runner.set_breakpoint(5)).toBe(true);
ctrl.debug();
await next_animation_frame();
expect(quest_editor_store.quest_runner.pause_location.val).toBe(5);
expect(ctrl.can_step.val).toBe(true);
expect(ctrl.can_stop.val).toBe(true);
}));

View File

@ -4,11 +4,10 @@ import {
create_quest_editor_store,
} from "../../../test/src/quest_editor/stores/store_creation";
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
import { create_new_quest } from "../stores/quest_creation";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { Vector3 } from "three";
import { euler } from "../model/euler";
import { deg_to_rad } from "../../core/math";
import { load_default_quest_model } from "../../../test/src/utils";
test("When input values change, this should be reflected in the selected entity.", () =>
with_disposer(disposer => {
@ -16,7 +15,7 @@ test("When input values change, this should be reflected in the selected entity.
const store = create_quest_editor_store(disposer, area_store);
const ctrl = new EntityInfoController(store);
const quest = create_new_quest(area_store, Episode.I);
const quest = load_default_quest_model(area_store);
const entity = quest.objects.get(0);
entity.set_position(new Vector3(0, 0, 0));
entity.set_rotation(euler(0, 0, 0));

View File

@ -2,13 +2,13 @@ import { Controller } from "../../core/controllers/Controller";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { Property } from "../../core/observable/property/Property";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { entity_data } from "../../core/data_formats/parsing/quest/entities";
import { property } from "../../core/observable";
import { Euler, Vector3 } from "three";
import { deg_to_rad } from "../../core/math";
import { TranslateEntityAction } from "../actions/TranslateEntityAction";
import { RotateEntityAction } from "../actions/RotateEntityAction";
import { euler } from "../model/euler";
import { entity_data } from "../../core/data_formats/parsing/quest/Quest";
const DUMMY_VECTOR = Object.freeze(new Vector3());
const DUMMY_EULER = Object.freeze(new Euler());

View File

@ -6,14 +6,22 @@ import {
import { QuestEditorToolBarController } from "./QuestEditorToolBarController";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
import { QuestLoader } from "../loading/QuestLoader";
import { FileSystemHttpClient } from "../../../test/src/core/FileSystemHttpClient";
test("Some widgets should only be enabled when a quest is loaded.", async () =>
with_disposer(async disposer => {
const quest_loader = disposer.add(new QuestLoader(new FileSystemHttpClient()));
const gui_store = disposer.add(new GuiStore());
const area_store = create_area_store(disposer);
const quest_editor_store = create_quest_editor_store(disposer, area_store);
const ctrl = disposer.add(
new QuestEditorToolBarController(gui_store, area_store, quest_editor_store),
new QuestEditorToolBarController(
quest_loader,
gui_store,
area_store,
quest_editor_store,
),
);
expect(ctrl.can_save.val).toBe(false);

View File

@ -7,12 +7,10 @@ import { Property } from "../../core/observable/property/Property";
import { undo_manager } from "../../core/undo/UndoManager";
import { Controller } from "../../core/controllers/Controller";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { create_new_quest } from "../stores/quest_creation";
import { open_files, read_file } from "../../core/files";
import {
parse_bin_dat_to_quest,
parse_qst_to_quest,
Quest,
write_quest_qst,
} from "../../core/data_formats/parsing/quest";
import { ArrayBufferCursor } from "../../core/data_formats/block/cursor/ArrayBufferCursor";
@ -24,6 +22,8 @@ import { Version } from "../../core/data_formats/parsing/quest/Version";
import { WritableProperty } from "../../core/observable/property/WritableProperty";
import { failure, Result } from "../../core/Result";
import { Severity } from "../../core/Severity";
import { Quest } from "../../core/data_formats/parsing/quest/Quest";
import { QuestLoader } from "../loading/QuestLoader";
const logger = LogManager.get("quest_editor/controllers/QuestEditorToolBarController");
@ -55,6 +55,7 @@ export class QuestEditorToolBarController extends Controller {
readonly version: Property<Version> = this._version;
constructor(
private readonly quest_loader: QuestLoader,
gui_store: GuiStore,
private readonly area_store: AreaStore,
private readonly quest_editor_store: QuestEditorStore,
@ -107,7 +108,7 @@ export class QuestEditorToolBarController extends Controller {
this.disposables(
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);
await this.parse_files(files);
}),
gui_store.on_global_keydown(GuiTool.QuestEditor, "Ctrl-Shift-S", this.save_as_clicked),
@ -129,7 +130,12 @@ export class QuestEditorToolBarController extends Controller {
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));
await this.quest_editor_store.set_current_quest(
convert_quest_to_model(
this.area_store,
await this.quest_loader.load_default_quest(episode),
),
);
};
parse_files = async (files: File[]): Promise<void> => {

View File

@ -3,9 +3,8 @@ import {
create_quest_editor_store,
} from "../../../test/src/quest_editor/stores/store_creation";
import { QuestInfoController } from "./QuestInfoController";
import { create_new_quest } from "../stores/quest_creation";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
import { load_default_quest_model } from "../../../test/src/utils";
test("When a property's input value changes, this should be reflected in the current quest object and the undo stack.", async () =>
with_disposer(async disposer => {
@ -13,7 +12,7 @@ test("When a property's input value changes, this should be reflected in the cur
const store = create_quest_editor_store(disposer, area_store);
const ctrl = disposer.add(new QuestInfoController(store));
await store.set_current_quest(create_new_quest(area_store, Episode.I));
await store.set_current_quest(load_default_quest_model(area_store));
ctrl.set_id(3004);
expect(store.current_quest.val!.id.val).toBe(3004);

View File

@ -4,10 +4,9 @@ import {
create_area_store,
create_quest_editor_store,
} from "../../../test/src/quest_editor/stores/store_creation";
import { create_new_quest } from "../stores/quest_creation";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
import { undo_manager } from "../../core/undo/UndoManager";
import { load_default_quest_model } from "../../../test/src/utils";
test("Renders correctly without an entity selected.", () => {
with_disposer(disposer => {
@ -19,7 +18,7 @@ test("Renders correctly without an entity selected.", () => {
expect(view.element).toMatchSnapshot('should render a "No entity selected." view');
store.set_current_quest(create_new_quest(area_store, Episode.I));
store.set_current_quest(load_default_quest_model(area_store));
expect(view.element).toMatchSnapshot('should render a "No entity selected." view');
});
@ -33,7 +32,7 @@ test("Renders correctly with an entity selected.", () => {
new EntityInfoView(disposer.add(new EntityInfoController(store))),
);
const quest = create_new_quest(area_store, Episode.I);
const quest = load_default_quest_model(area_store);
store.set_current_quest(quest);
store.set_selected_entity(quest.npcs.get(0));

View File

@ -1,6 +1,6 @@
import { bind_children_to, div, img, span } from "../../core/gui/dom";
import "./EntityListView.css";
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities";
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/Quest";
import { entity_dnd_source } from "./entity_dnd";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { list_property } from "../../core/observable";

View File

@ -5,8 +5,7 @@ import {
create_quest_editor_store,
} from "../../../test/src/quest_editor/stores/store_creation";
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
import { create_new_quest } from "../stores/quest_creation";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { load_default_quest_model } from "../../../test/src/utils";
test("Renders correctly without a current quest.", () =>
with_disposer(disposer => {
@ -22,7 +21,7 @@ test("Renders correctly with a current quest.", () =>
const store = create_quest_editor_store(disposer, area_store);
const view = disposer.add(new NpcCountsView(disposer.add(new NpcCountsController(store))));
store.set_current_quest(create_new_quest(area_store, Episode.I));
store.set_current_quest(load_default_quest_model(area_store));
expect(view.element).toMatchSnapshot("Should render a table with NPC names and counts.");
}));

View File

@ -5,9 +5,12 @@ import { create_area_store } from "../../../test/src/quest_editor/stores/store_c
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
import { LogStore } from "../stores/LogStore";
import { QuestLoader } from "../loading/QuestLoader";
import { StubHttpClient } from "../../core/HttpClient";
test("Renders correctly.", () =>
with_disposer(disposer => {
const quest_loader = disposer.add(new QuestLoader(new StubHttpClient()));
const gui_store = disposer.add(new GuiStore());
const area_store = create_area_store(disposer);
const log_store = disposer.add(new LogStore());
@ -17,7 +20,12 @@ test("Renders correctly.", () =>
const tool_bar = disposer.add(
new QuestEditorToolBarView(
disposer.add(
new QuestEditorToolBarController(gui_store, area_store, quest_editor_store),
new QuestEditorToolBarController(
quest_loader,
gui_store,
area_store,
quest_editor_store,
),
),
),
);

View File

@ -1,13 +1,12 @@
import { QuestInfoController } from "../controllers/QuestInfoController";
import { undo_manager } from "../../core/undo/UndoManager";
import { QuestInfoView } from "./QuestInfoView";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import {
create_area_store,
create_quest_editor_store,
} from "../../../test/src/quest_editor/stores/store_creation";
import { create_new_quest } from "../stores/quest_creation";
import { with_disposer } from "../../../test/src/core/observables/disposable_helpers";
import { load_default_quest_model } from "../../../test/src/utils";
test("Renders correctly without a current quest.", () =>
with_disposer(disposer => {
@ -26,7 +25,7 @@ test("Renders correctly with a current quest.", () =>
const store = create_quest_editor_store(disposer);
const view = disposer.add(new QuestInfoView(disposer.add(new QuestInfoController(store))));
await store.set_current_quest(create_new_quest(area_store, Episode.I));
await store.set_current_quest(load_default_quest_model(area_store));
expect(view.element).toMatchSnapshot("should render property inputs");
}));

View File

@ -1,4 +1,4 @@
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/entities";
import { entity_data, EntityType } from "../../core/data_formats/parsing/quest/Quest";
import { Disposable } from "../../core/observable/Disposable";
import { Vector2 } from "three";
import { div } from "../../core/gui/dom";

View File

@ -30,6 +30,7 @@ import { EventsController } from "./controllers/EventsController";
import { DebugView } from "./gui/DebugView";
import { DebugController } from "./controllers/DebugController";
import { LogStore } from "./stores/LogStore";
import { QuestLoader } from "./loading/QuestLoader";
export function initialize_quest_editor(
http_client: HttpClient,
@ -39,6 +40,7 @@ export function initialize_quest_editor(
const disposer = new Disposer();
// Asset Loaders
const quest_loader = disposer.add(new QuestLoader(http_client));
const area_asset_loader = disposer.add(new AreaAssetLoader(http_client));
const entity_asset_loader = disposer.add(new EntityAssetLoader(http_client));
@ -65,7 +67,12 @@ export function initialize_quest_editor(
disposer.add(
new QuestEditorToolBarView(
disposer.add(
new QuestEditorToolBarController(gui_store, area_store, quest_editor_store),
new QuestEditorToolBarController(
quest_loader,
gui_store,
area_store,
quest_editor_store,
),
),
),
),

View File

@ -12,7 +12,7 @@ import {
entity_type_to_string,
EntityType,
is_npc_type,
} from "../../core/data_formats/parsing/quest/entities";
} from "../../core/data_formats/parsing/quest/Quest";
import { HttpClient } from "../../core/HttpClient";
import { Disposable } from "../../core/observable/Disposable";
import { LogManager } from "../../core/Logger";
@ -424,13 +424,13 @@ function entity_type_to_url(type: EntityType, asset_type: AssetType, no?: number
case ObjectType.FallingRock:
case ObjectType.DesertFixedTypeBoxBreakableCrystals:
case ObjectType.BeeHive:
return `/objects/${object_data(type).pso_id}${no_str}.nj`;
return `/objects/${object_data(type).type_id}${no_str}.nj`;
default:
return `/objects/${object_data(type).pso_id}${no_str}.xj`;
return `/objects/${object_data(type).type_id}${no_str}.xj`;
}
} else {
return `/objects/${object_data(type).pso_id}${no_str}.xvm`;
return `/objects/${object_data(type).type_id}${no_str}.xvm`;
}
}
}

View File

@ -0,0 +1,37 @@
import { Disposable } from "../../core/observable/Disposable";
import { LoadingCache } from "./LoadingCache";
import { HttpClient } from "../../core/HttpClient";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { DisposablePromise } from "../../core/DisposablePromise";
import { parse_qst_to_quest } from "../../core/data_formats/parsing/quest";
import { ArrayBufferCursor } from "../../core/data_formats/block/cursor/ArrayBufferCursor";
import { Endianness } from "../../core/data_formats/block/Endianness";
import { assert } from "../../core/util";
import { Quest } from "../../core/data_formats/parsing/quest/Quest";
export class QuestLoader implements Disposable {
private readonly cache = new LoadingCache<string, ArrayBuffer>();
constructor(private readonly http_client: HttpClient) {}
load_default_quest(episode: Episode): DisposablePromise<Quest> {
if (episode === Episode.II) throw new Error("Episode II not yet supported.");
if (episode === Episode.IV) throw new Error("Episode IV not yet supported.");
return this.load_quest("/defaults/default_ep_1.qst");
}
dispose(): void {
this.cache.dispose();
}
private load_quest(path: string): DisposablePromise<Quest> {
return this.cache
.get_or_set(path, () => this.http_client.get(`/quests${path}`).array_buffer())
.then(buffer => {
const result = parse_qst_to_quest(new ArrayBufferCursor(buffer, Endianness.Little));
assert(result, () => `Quest "${path}" can't be parsed.`);
return result.quest;
});
}
}

View File

@ -1,5 +1,5 @@
import { QuestNpcModel } from "./QuestNpcModel";
import { npc_data, NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { Vector3 } from "three";
import { SectionModel } from "./SectionModel";
@ -8,6 +8,7 @@ import { AreaStore } from "../stores/AreaStore";
import { StubHttpClient } from "../../core/HttpClient";
import { AreaAssetLoader } from "../loading/AreaAssetLoader";
import { euler } from "./euler";
import { QuestNpc } from "../../core/data_formats/parsing/quest/Quest";
const area_store = new AreaStore(new AreaAssetLoader(new StubHttpClient()));
@ -43,19 +44,9 @@ test("After changing section, world position should change accordingly.", () =>
});
function create_entity(): QuestEntityModel {
return new QuestNpcModel(
NpcType.AlRappy,
npc_data(NpcType.AlRappy).pso_type_id!,
1,
undefined,
0,
0,
0,
area_store.get_area(Episode.I, 0).id,
20,
new Vector3(5, 5, 5),
euler(0, 0, 0),
new Vector3(1, 1, 1),
[Array(10).fill(0xdead), Array(4).fill(0xdead)],
const entity = new QuestNpcModel(
QuestNpc.create(NpcType.AlRappy, area_store.get_area(Episode.I, 0).id, 0),
);
entity.set_position(new Vector3(5, 5, 5));
return entity;
}

View File

@ -1,18 +1,21 @@
import { EntityType } from "../../core/data_formats/parsing/quest/entities";
import { EntityType, QuestEntity } from "../../core/data_formats/parsing/quest/Quest";
import { Property } from "../../core/observable/property/Property";
import { property } from "../../core/observable";
import { WritableProperty } from "../../core/observable/property/WritableProperty";
import { SectionModel } from "./SectionModel";
import { Euler, Quaternion, Vector3 } from "three";
import { floor_mod } from "../../core/math";
import { defined, require_integer } from "../../core/util";
import { euler_from_quat } from "./euler";
import { euler, euler_from_quat } from "./euler";
import { vec3_to_threejs } from "../../core/rendering/conversion";
// These quaternions are used as temporary variables to avoid memory allocation.
const q1 = new Quaternion();
const q2 = new Quaternion();
export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
export abstract class QuestEntityModel<
Type extends EntityType = EntityType,
Entity extends QuestEntity<Type> = QuestEntity<Type>
> {
private readonly _section_id: WritableProperty<number>;
private readonly _section: WritableProperty<SectionModel | undefined> = property(undefined);
private readonly _position: WritableProperty<Vector3>;
@ -20,9 +23,19 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
private readonly _rotation: WritableProperty<Euler>;
private readonly _world_rotation: WritableProperty<Euler>;
readonly type: Type;
/**
* Many modifications done to the underlying entity directly will not be reflected in this
* model's properties.
*/
readonly entity: Entity;
readonly area_id: number;
get type(): Type {
return this.entity.type;
}
get area_id(): number {
return this.entity.area_id;
}
readonly section_id: Property<number>;
@ -42,32 +55,25 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
readonly world_rotation: Property<Euler>;
protected constructor(
type: Type,
area_id: number,
section_id: number,
position: Vector3,
rotation: Euler,
) {
defined(type, "type");
require_integer(area_id, "area_id");
require_integer(section_id, "section_id");
defined(position, "position");
defined(rotation, "rotation");
protected constructor(entity: Entity) {
this.entity = entity;
this.type = type;
this.area_id = area_id;
this.section = this._section;
this._section_id = property(section_id);
this._section_id = property(entity.section_id);
this.section_id = this._section_id;
const position = vec3_to_threejs(entity.position);
this._position = property(position);
this.position = this._position;
this._world_position = property(position);
this.world_position = this._world_position;
const { x: rot_x, y: rot_y, z: rot_z } = entity.rotation;
const rotation = euler(rot_x, rot_y, rot_z);
this._rotation = property(rotation);
this.rotation = this._rotation;
@ -80,6 +86,8 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
throw new Error(`Quest entities can't be moved across areas.`);
}
this.entity.section_id = section.id;
this._section.val = section;
this._section_id.val = section.id;
@ -90,18 +98,15 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
}
set_position(pos: Vector3): this {
this.entity.position = pos;
this._position.val = pos;
const section = this.section.val;
if (section) {
this._world_position.val = pos
.clone()
.applyEuler(section.rotation)
.add(section.position);
} else {
this._world_position.val = pos;
}
this._world_position.val = section
? pos.clone().applyEuler(section.rotation).add(section.position)
: pos;
return this;
}
@ -111,14 +116,12 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
const section = this.section.val;
if (section) {
this._position.val = pos
.clone()
.sub(section.position)
.applyEuler(section.inverse_rotation);
} else {
this._position.val = pos;
}
const rel_pos = section
? pos.clone().sub(section.position).applyEuler(section.inverse_rotation)
: pos;
this.entity.position = rel_pos;
this._position.val = rel_pos;
return this;
}
@ -126,6 +129,8 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
set_rotation(rot: Euler): this {
floor_mod_euler(rot);
this.entity.rotation = rot;
this._rotation.val = rot;
const section = this.section.val;
@ -148,16 +153,21 @@ export abstract class QuestEntityModel<Type extends EntityType = EntityType> {
const section = this.section.val;
let rel_rot: Euler;
if (section) {
q1.setFromEuler(rot);
q2.setFromEuler(section.rotation);
q2.inverse();
this._rotation.val = floor_mod_euler(euler_from_quat(q1.multiply(q2)));
rel_rot = floor_mod_euler(euler_from_quat(q1.multiply(q2)));
} else {
this._rotation.val = rot;
rel_rot = rot;
}
this.entity.rotation = rel_rot;
this._rotation.val = rel_rot;
return this;
}
}

View File

@ -10,7 +10,7 @@ import { AreaVariantModel } from "./AreaVariantModel";
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 { entity_type_to_string } from "../../core/data_formats/parsing/quest/Quest";
import { QuestEventDagModel } from "./QuestEventDagModel";
import { assert, defined, require_array } from "../../core/util";
import { AreaStore } from "../stores/AreaStore";

View File

@ -1,109 +1,30 @@
import { QuestEntityModel } from "./QuestEntityModel";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { Euler, Vector3 } from "three";
import { WritableProperty } from "../../core/observable/property/WritableProperty";
import { Property } from "../../core/observable/property/Property";
import {
assert,
defined,
require_finite,
require_integer,
require_non_negative_integer,
} from "../../core/util";
import { defined } from "../../core/util";
import { property } from "../../core/observable";
import { WaveModel } from "./WaveModel";
import { QuestNpc } from "../../core/data_formats/parsing/quest/Quest";
export class QuestNpcModel extends QuestEntityModel<NpcType> {
export class QuestNpcModel extends QuestEntityModel<NpcType, QuestNpc> {
private readonly _wave: WritableProperty<WaveModel | undefined>;
private readonly _pso_wave2: number;
readonly pso_type_id: number;
readonly npc_id: number;
readonly wave: Property<WaveModel | undefined>;
readonly pso_wave2: Property<number>;
readonly script_label: number;
readonly pso_roaming: number;
readonly scale: Vector3;
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: readonly number[][];
constructor(npc: QuestNpc, wave?: WaveModel) {
defined(npc, "npc");
constructor(
type: NpcType,
pso_type_id: number,
npc_id: number,
wave: WaveModel | undefined,
pso_wave2: number,
script_label: number,
pso_roaming: number,
area_id: number,
section_id: number,
position: Vector3,
rotation: Euler,
scale: Vector3,
unknown: readonly number[][],
) {
require_integer(pso_type_id, "pso_type_id");
require_finite(npc_id, "npc_id");
require_non_negative_integer(pso_wave2, "pso_wave2");
require_integer(script_label, "script_label");
require_integer(pso_roaming, "pso_roaming");
defined(scale, "scale");
defined(unknown, "unknown");
assert(unknown.length === 2, () => `unknown should be of length 2, was ${unknown.length}.`);
assert(
unknown[0].length === 10,
() => `unknown[0] should be of length 10, was ${unknown[0].length}`,
);
assert(
unknown[1].length === 4,
() => `unknown[1] should be of length 4, was ${unknown[1].length}`,
);
super(type, area_id, section_id, position, rotation);
this.pso_type_id = pso_type_id;
this.npc_id = npc_id;
super(npc);
this._wave = property(wave);
this.wave = this._wave;
// pso_wave2 should stay as it is originally until wave or wave.id changes.
const orig_wave = {
id: wave?.id?.val,
section_id: wave?.section_id?.val,
area_id: wave?.area_id?.val,
};
this._pso_wave2 = pso_wave2;
this.pso_wave2 = this.wave.flat_map(wave => {
if (wave == undefined) {
return property(orig_wave.id == undefined ? this._pso_wave2 : 0);
} else if (
orig_wave.id === wave.id.val &&
orig_wave.section_id === wave.section_id.val &&
orig_wave.area_id === wave.area_id.val
) {
return wave.id.map(wave_id => {
if (orig_wave.id === wave_id) {
return this._pso_wave2;
} else {
return wave_id;
}
});
} else {
return wave.id;
}
});
this.script_label = script_label;
this.pso_roaming = pso_roaming;
this.unknown = unknown;
this.scale = scale;
}
set_wave(wave?: WaveModel): this {
const wave_id = wave?.id?.val ?? 0;
this.entity.wave = wave_id;
this.entity.wave_2 = wave_id;
this._wave.val = wave;
return this;
}

View File

@ -1,32 +1,12 @@
import { QuestEntityModel } from "./QuestEntityModel";
import { ObjectType } from "../../core/data_formats/parsing/quest/object_types";
import { Euler, Vector3 } from "three";
import { QuestObject } from "../../core/data_formats/parsing/quest/Quest";
import { defined } from "../../core/util";
export class QuestObjectModel extends QuestEntityModel<ObjectType> {
readonly id: number;
readonly group_id: number;
readonly properties: Map<string, number>;
/**
* Data of which the purpose hasn't been discovered yet.
*/
readonly unknown: readonly number[][];
export class QuestObjectModel extends QuestEntityModel<ObjectType, QuestObject> {
constructor(object: QuestObject) {
defined(object, "object");
constructor(
type: ObjectType,
id: number,
group_id: number,
area_id: number,
section_id: number,
position: Vector3,
rotation: Euler,
properties: Map<string, number>,
unknown: readonly number[][],
) {
super(type, area_id, section_id, position, rotation);
this.id = id;
this.group_id = group_id;
this.properties = properties;
this.unknown = unknown;
super(object);
}
}

View File

@ -6,7 +6,7 @@ import {
Scene,
Vector3,
} from "three";
import { EntityType } from "../../core/data_formats/parsing/quest/entities";
import { EntityType } from "../../core/data_formats/parsing/quest/Quest";
import { create_entity_type_mesh } from "./conversion/entities";
import { sequential } from "../../core/sequential";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";

View File

@ -12,7 +12,7 @@ import {
} from "../../core/observable/property/list/ListProperty";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { QuestObjectModel } from "../model/QuestObjectModel";
import { entity_type_to_string } from "../../core/data_formats/parsing/quest/entities";
import { entity_type_to_string } from "../../core/data_formats/parsing/quest/Quest";
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { AreaVariantModel } from "../model/AreaVariantModel";
import { EntityAssetLoader } from "../loading/EntityAssetLoader";

View File

@ -7,8 +7,12 @@ import { AreaUserData } from "./conversion/areas";
import { SectionModel } from "../model/SectionModel";
import { Disposable } from "../../core/observable/Disposable";
import { Disposer } from "../../core/observable/Disposer";
import { EntityType, is_npc_type } from "../../core/data_formats/parsing/quest/entities";
import { npc_data } from "../../core/data_formats/parsing/quest/npc_types";
import {
EntityType,
is_npc_type,
QuestNpc,
QuestObject,
} from "../../core/data_formats/parsing/quest/Quest";
import {
add_entity_dnd_listener,
EntityDragEvent,
@ -18,7 +22,6 @@ import { QuestObjectModel } from "../model/QuestObjectModel";
import { AreaModel } from "../model/AreaModel";
import { QuestModel } from "../model/QuestModel";
import { QuestEditorStore } from "../stores/QuestEditorStore";
import { euler } from "../model/euler";
import { CreateEntityAction } from "../actions/CreateEntityAction";
import { RotateEntityAction } from "../actions/RotateEntityAction";
import { RemoveEntityAction } from "../actions/RemoveEntityAction";
@ -637,44 +640,14 @@ class CreationState implements State {
}
if (is_npc_type(evt.entity_type)) {
const data = npc_data(evt.entity_type);
const wave = quest_editor_store.selected_wave.val;
this.entity = new QuestNpcModel(
evt.entity_type,
data.pso_type_id!,
0,
quest_editor_store.selected_wave.val,
0,
0,
data.pso_roaming!,
area.id,
0,
new Vector3(0, 0, 0),
euler(0, 0, 0),
new Vector3(1, 1, 1),
// TODO: do the following values make sense?
[
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 0, 0, 0],
],
QuestNpc.create(evt.entity_type, area.id, wave?.id.val ?? 0),
wave,
);
} else {
this.entity = new QuestObjectModel(
evt.entity_type,
0,
0,
area.id,
0,
new Vector3(0, 0, 0),
euler(0, 0, 0),
// TODO: which default properties?
new Map(),
// TODO: do the following values make sense?
[
[0, 0, 0, 0, 0, 0],
[0, 0],
],
);
this.entity = new QuestObjectModel(QuestObject.create(evt.entity_type, area.id));
}
translate_entity_horizontally(

View File

@ -5,7 +5,7 @@ import {
entity_type_to_string,
EntityType,
is_npc_type,
} from "../../../core/data_formats/parsing/quest/entities";
} from "../../../core/data_formats/parsing/quest/Quest";
export enum ColorType {
Normal,

View File

@ -1,7 +1,5 @@
import { Quest } from "../../core/data_formats/parsing/quest";
import { QuestModel } from "../model/QuestModel";
import { QuestObjectModel } from "../model/QuestObjectModel";
import { vec3_to_threejs } from "../../core/rendering/conversion";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { QuestEventModel } from "../model/QuestEventModel";
import {
@ -16,11 +14,10 @@ import {
QuestEventActionUnlockModel,
} from "../model/QuestEventActionModel";
import { QuestEventDagModel } from "../model/QuestEventDagModel";
import { QuestEvent, QuestNpc } from "../../core/data_formats/parsing/quest/entities";
import { Quest, QuestEvent, QuestNpc } from "../../core/data_formats/parsing/quest/Quest";
import { clone_segment } from "../../core/data_formats/asm/instructions";
import { AreaStore } from "./AreaStore";
import { LogManager } from "../../core/Logger";
import { euler } from "../model/euler";
import { WaveModel } from "../model/WaveModel";
const logger = LogManager.get("quest_editor/stores/model_conversion");
@ -37,20 +34,7 @@ export function convert_quest_to_model(area_store: AreaStore, quest: Quest): Que
quest.long_description,
quest.episode,
quest.map_designations,
quest.objects.map(
obj =>
new QuestObjectModel(
obj.type,
obj.id,
obj.group_id,
obj.area_id,
obj.section_id,
vec3_to_threejs(obj.position),
euler(obj.rotation.x, obj.rotation.y, obj.rotation.z),
obj.properties,
obj.unknown,
),
),
quest.objects.map(obj => new QuestObjectModel(obj)),
quest.npcs.map(npc => convert_npc_to_model(wave_cache, npc)),
build_event_dags(wave_cache, quest.events),
quest.dat_unknowns,
@ -63,21 +47,7 @@ function convert_npc_to_model(wave_cache: Map<string, WaveModel>, npc: QuestNpc)
const wave =
npc.wave === 0 ? undefined : get_wave(wave_cache, npc.area_id, npc.section_id, npc.wave);
return new QuestNpcModel(
npc.type,
npc.pso_type_id,
npc.npc_id,
wave,
npc.pso_wave2,
npc.script_label,
npc.pso_roaming,
npc.area_id,
npc.section_id,
vec3_to_threejs(npc.position),
euler(npc.rotation.x, npc.rotation.y, npc.rotation.z),
vec3_to_threejs(npc.scale),
npc.unknown,
);
return new QuestNpcModel(npc, wave);
}
function get_wave(
@ -227,32 +197,8 @@ export function convert_quest_from_model(quest: QuestModel): Quest {
short_description: quest.short_description.val,
long_description: quest.long_description.val,
episode: quest.episode,
objects: quest.objects.val.map(obj => ({
type: obj.type,
area_id: obj.area_id,
section_id: obj.section_id.val,
position: obj.position.val.clone(),
rotation: obj.rotation.val.clone(),
unknown: obj.unknown,
id: obj.id,
group_id: obj.group_id,
properties: new Map(obj.properties),
})),
npcs: quest.npcs.val.map(npc => ({
type: npc.type,
area_id: npc.area_id,
section_id: npc.section_id.val,
wave: npc.wave.val?.id.val ?? 0,
pso_wave2: npc.pso_wave2.val,
position: npc.position.val.clone(),
rotation: npc.rotation.val.clone(),
scale: npc.scale.clone(),
unknown: npc.unknown,
pso_type_id: npc.pso_type_id,
npc_id: npc.npc_id,
script_label: npc.script_label,
pso_roaming: npc.pso_roaming,
})),
objects: quest.objects.val.map(obj => obj.entity),
npcs: quest.npcs.val.map(npc => npc.entity),
events: convert_quest_events_from_model(quest.event_dags),
dat_unknowns: quest.dat_unknowns.map(unk => ({ ...unk })),
object_code: quest.object_code.map(seg => clone_segment(seg)),

View File

@ -1,942 +0,0 @@
import { Episode } from "../../core/data_formats/parsing/quest/Episode";
import { QuestModel } from "../model/QuestModel";
import { ObjectType } from "../../core/data_formats/parsing/quest/object_types";
import { NpcType } from "../../core/data_formats/parsing/quest/npc_types";
import { QuestObjectModel } from "../model/QuestObjectModel";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { Euler, Vector3 } from "three";
import { QuestEventDagModel } from "../model/QuestEventDagModel";
import { AreaStore } from "./AreaStore";
import { assemble } from "../scripting/assembly";
import { Segment } from "../../core/data_formats/asm/instructions";
import { euler } from "../model/euler";
export function create_new_quest(area_store: AreaStore, episode: Episode): QuestModel {
if (episode === Episode.II) throw new Error("Episode II not yet supported.");
if (episode === Episode.IV) throw new Error("Episode IV not yet supported.");
return new QuestModel(
area_store,
0,
0,
"Untitled",
"Created with phantasmal.world.",
"Created with phantasmal.world.",
episode,
new Map().set(0, 0),
create_default_objects(),
create_default_npcs(),
create_default_event_dags(),
[],
create_default_object_code(),
[],
);
}
function create_default_objects(): QuestObjectModel[] {
return [
new QuestObjectModel(
ObjectType.MenuActivation,
16384,
0,
0,
10,
new Vector3(-16.313568115234375, 3, -579.5118408203125),
euler(0.0009587526218325454, 0, 0),
new Map([
["property_0", 1],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365279104],
]),
[
[2, 0, 0, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.MenuActivation,
16385,
0,
0,
10,
new Vector3(-393.07318115234375, 10, -12.964752197265625),
euler(0, 0, 0),
new Map([
["property_0", 9.183549615799121e-41],
["property_1", 1.0000011920928955],
["property_2", 1],
["property_3", 0],
["property_4", 0],
["property_5", 0],
["property_6", 2365279264],
]),
[
[2, 0, 1, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.MenuActivation,
16386,
0,
0,
10,
new Vector3(-458.60699462890625, 10, -51.270660400390625),
euler(0, 0, 0),
new Map([
["property_0", 1],
["property_1", 1],
["property_2", 1],
["property_3", 2],
["property_4", 65536],
["property_5", 10],
["property_6", 2365279424],
]),
[
[2, 0, 2, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.MenuActivation,
16387,
0,
0,
10,
new Vector3(-430.19696044921875, 10, -24.490447998046875),
euler(0, 0, 0),
new Map([
["property_0", 1],
["property_1", 1],
["property_2", 1],
["property_3", 3],
["property_4", 0],
["property_5", 0],
["property_6", 2365279584],
]),
[
[2, 0, 3, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PlayerSet,
16388,
0,
0,
10,
new Vector3(0.995330810546875, 0, -37.0010986328125),
euler(0, 4.712460886831327, 0),
new Map([
["property_0", 0],
["property_1", 1],
["property_2", 1],
["property_3", 0],
["property_4", 0],
["property_5", 0],
["property_6", 2365279744],
]),
[
[2, 0, 4, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PlayerSet,
16389,
0,
0,
10,
new Vector3(3.0009307861328125, 0, -23.99688720703125),
euler(0, 4.859725289544806, 0),
new Map([
["property_0", 1.000000238418579],
["property_1", 1],
["property_2", 1],
["property_3", 0],
["property_4", 0],
["property_5", 0],
["property_6", 2365279904],
]),
[
[2, 0, 5, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PlayerSet,
16390,
0,
0,
10,
new Vector3(2.0015106201171875, 0, -50.00386047363281),
euler(0, 4.565196484117848, 0),
new Map([
["property_0", 2.000002384185791],
["property_1", 1],
["property_2", 1],
["property_3", 0],
["property_4", 0],
["property_5", 0],
["property_6", 2365280064],
]),
[
[2, 0, 6, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PlayerSet,
16391,
0,
0,
10,
new Vector3(4.9973907470703125, 0, -61.99664306640625),
euler(0, 4.368843947166543, 0),
new Map([
["property_0", 3.0000007152557373],
["property_1", 1],
["property_2", 1],
["property_3", 65536],
["property_4", 10],
["property_5", 0],
["property_6", 2365280224],
]),
[
[2, 0, 7, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.MainRagolTeleporter,
18264,
0,
0,
10,
new Vector3(132.00314331054688, 1.000000238418579, -265.002197265625),
euler(0, 0.49088134237826325, 0),
new Map([
["property_0", 1.000000238418579],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365227216],
]),
[
[0, 0, 87, 7, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PrincipalWarp,
16393,
0,
0,
10,
new Vector3(-228, 0, -2020.99951171875),
euler(0, 2.9452880542695796, 0),
new Map([
["property_0", -10.000004768371582],
["property_1", 0],
["property_2", -30.000030517578125],
["property_3", 0],
["property_4", 65536],
["property_5", 65536],
["property_6", 2365280688],
]),
[
[2, 0, 9, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.MenuActivation,
16394,
0,
0,
10,
new Vector3(-41.000030517578125, 0, 42.37322998046875),
euler(0, 0, 0),
new Map([
["property_0", 1],
["property_1", 1],
["property_2", 1],
["property_3", 4],
["property_4", 0],
["property_5", 0],
["property_6", 2365280992],
]),
[
[2, 0, 10, 0, 0, 0],
[1, 0],
],
),
new QuestObjectModel(
ObjectType.MenuActivation,
16395,
0,
0,
10,
new Vector3(-479.21673583984375, 8.781256675720215, -322.465576171875),
euler(6.28328118244177, 0.0009587526218325454, 0),
new Map([
["property_0", 1],
["property_1", 1],
["property_2", 1],
["property_3", 5],
["property_4", 0],
["property_5", 0],
["property_6", 2365281152],
]),
[
[2, 0, 11, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PrincipalWarp,
16396,
0,
0,
10,
new Vector3(-228, 0, -351.0015869140625),
euler(0, 0, 0),
new Map([
["property_0", 10.000006675720215],
["property_1", 0],
["property_2", -1760.0010986328125],
["property_3", 32768],
["property_4", 0],
["property_5", 0],
["property_6", 2365281312],
]),
[
[2, 0, 12, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.TelepipeLocation,
16397,
0,
0,
10,
new Vector3(-561.88232421875, 0, -406.8829345703125),
euler(0, 0, 0),
new Map([
["property_0", 1],
["property_1", 1],
["property_2", 1],
["property_3", 0],
["property_4", 0],
["property_5", 0],
["property_6", 2365281616],
]),
[
[2, 0, 13, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.TelepipeLocation,
16398,
0,
0,
10,
new Vector3(-547.8557739257812, 0, -444.8822326660156),
euler(0, 0, 0),
new Map([
["property_0", 1],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365281808],
]),
[
[2, 0, 14, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.TelepipeLocation,
16399,
0,
0,
10,
new Vector3(-486.441650390625, 0, -497.4501647949219),
euler(0, 0, 0),
new Map([
["property_0", 9.183549615799121e-41],
["property_1", 1.0000011920928955],
["property_2", 1],
["property_3", 3],
["property_4", 0],
["property_5", 0],
["property_6", 2365282000],
]),
[
[2, 0, 15, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.TelepipeLocation,
16400,
0,
0,
10,
new Vector3(-522.4052734375, 0, -474.1882629394531),
euler(0, 0, 0),
new Map([
["property_0", 1],
["property_1", 1],
["property_2", 1],
["property_3", 2],
["property_4", 65536],
["property_5", 10],
["property_6", 2365282192],
]),
[
[2, 0, 16, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.MedicalCenterDoor,
16401,
0,
0,
10,
new Vector3(-34.49853515625, 0, -384.4951171875),
euler(0, 5.497871034636549, 0),
new Map([
["property_0", 3.0000007152557373],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365282384],
]),
[
[2, 0, 17, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.ShopDoor,
16402,
0,
0,
10,
new Vector3(-393.0031433105469, 0, -143.49981689453125),
euler(0, 3.141640591220885, 0),
new Map([
["property_0", 3.0000007152557373],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365282640],
]),
[
[2, 0, 18, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.MenuActivation,
16403,
0,
0,
10,
new Vector3(-355.17462158203125, 0, -43.15193176269531),
euler(0, 0, 0),
new Map([
["property_0", 1.000000238418579],
["property_1", 1],
["property_2", 1],
["property_3", 6],
["property_4", 0],
["property_5", 0],
["property_6", 2365282896],
]),
[
[2, 0, 19, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.HuntersGuildDoor,
16404,
0,
0,
10,
new Vector3(-43.00239562988281, 0, -118.00120544433594),
euler(0, 3.141640591220885, 0),
new Map([
["property_0", 3.0000007152557373],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365283056],
]),
[
[2, 0, 20, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.TeleporterDoor,
16405,
0,
0,
10,
new Vector3(26.000823974609375, 0, -265.99810791015625),
euler(0, 3.141640591220885, 0),
new Map([
["property_0", 3.0000007152557373],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365283312],
]),
[
[2, 0, 21, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PlayerSet,
16406,
0,
0,
10,
new Vector3(57.81005859375, 0, -268.5472412109375),
euler(0, 4.712460886831327, 0),
new Map([
["property_0", 0],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365283568],
]),
[
[2, 0, 22, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PlayerSet,
16407,
0,
0,
10,
new Vector3(66.769287109375, 0, -252.3748779296875),
euler(0, 4.712460886831327, 0),
new Map([
["property_0", 1.000000238418579],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365283728],
]),
[
[2, 0, 23, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PlayerSet,
16408,
0,
0,
10,
new Vector3(67.36819458007812, 0, -284.9297180175781),
euler(0, 4.712460886831327, 0),
new Map([
["property_0", 2.000000476837158],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365283888],
]),
[
[2, 0, 24, 0, 0, 0],
[0, 0],
],
),
new QuestObjectModel(
ObjectType.PlayerSet,
16409,
0,
0,
10,
new Vector3(77.10488891601562, 0, -269.2830505371094),
euler(0, 4.712460886831327, 0),
new Map([
["property_0", 3.0000007152557373],
["property_1", 1],
["property_2", 1],
["property_3", 1],
["property_4", 0],
["property_5", 0],
["property_6", 2365284048],
]),
[
[2, 0, 25, 0, 0, 0],
[0, 0],
],
),
];
}
function create_default_npcs(): QuestNpcModel[] {
return [
new QuestNpcModel(
NpcType.GuildLady,
29,
1011.0010986328125,
undefined,
0,
850,
0,
0,
10,
new Vector3(-49.0010986328125, 0, 50.996429443359375),
euler(0, 2.3562304434156633, 0),
new Vector3(0, 0, 0),
[
[0, 0, 7, 86, 0, 0, 0, 0, 23, 87],
[128, 238, 223, 176],
],
),
new QuestNpcModel(
NpcType.FemaleFat,
4,
1016.0010986328125,
undefined,
0,
100,
1,
0,
20,
new Vector3(167.99769592285156, 0, 83.99686431884766),
euler(0, 3.927050739026106, 0),
new Vector3(24.000009536743164, 0, 0),
[
[0, 0, 7, 88, 0, 0, 0, 0, 23, 89],
[128, 238, 232, 48],
],
),
new QuestNpcModel(
NpcType.MaleDwarf,
10,
1015.0010986328125,
undefined,
0,
90,
1,
0,
20,
new Vector3(156.0028839111328, 0, -49.99967575073242),
euler(0, 5.497871034636549, 0),
new Vector3(30.000009536743164, 0, 0),
[
[0, 0, 7, 89, 0, 0, 0, 0, 23, 90],
[128, 238, 236, 176],
],
),
new QuestNpcModel(
NpcType.RedSoldier,
26,
1020.0010986328125,
undefined,
0,
130,
0,
0,
20,
new Vector3(237.9988250732422, 0, -14.0001220703125),
euler(0, 5.497871034636549, 0),
new Vector3(0, 0, 0),
[
[0, 0, 7, 90, 0, 0, 0, 0, 23, 91],
[128, 238, 241, 48],
],
),
new QuestNpcModel(
NpcType.BlueSoldier,
25,
1019.0010986328125,
undefined,
0,
120,
0,
0,
20,
new Vector3(238.00379943847656, 0, 63.00413513183594),
euler(0, 3.927050739026106, 0),
new Vector3(0, 0, 0),
[
[0, 0, 7, 91, 0, 0, 0, 0, 23, 92],
[128, 238, 245, 176],
],
),
new QuestNpcModel(
NpcType.FemaleMacho,
5,
1014.0010986328125,
undefined,
0,
80,
1,
0,
20,
new Vector3(-2.001882553100586, 0, 35.0036506652832),
euler(0, 3.141640591220885, 0),
new Vector3(26.000009536743164, 0, 0),
[
[0, 0, 7, 92, 0, 0, 0, 0, 23, 93],
[128, 238, 250, 48],
],
),
new QuestNpcModel(
NpcType.Scientist,
30,
1013.0010986328125,
undefined,
0,
70,
1,
0,
20,
new Vector3(-147.0000457763672, 0, -7.996537208557129),
euler(0, 2.577127047485882, 0),
new Vector3(30.000009536743164, 0, 0),
[
[0, 0, 7, 93, 0, 0, 0, 0, 23, 94],
[128, 238, 254, 176],
],
),
new QuestNpcModel(
NpcType.MaleOld,
13,
1012.0010986328125,
undefined,
0,
60,
1,
0,
20,
new Vector3(-219.99710083007812, 0, -100.0008316040039),
euler(0, 0, 0),
new Vector3(30.000011444091797, 0, 0),
[
[0, 0, 7, 94, 0, 0, 0, 0, 23, 95],
[128, 239, 3, 48],
],
),
new QuestNpcModel(
NpcType.GuildLady,
29,
1010.0010986328125,
undefined,
0,
840,
0,
0,
20,
new Vector3(-262.5099792480469, 0, -24.53999900817871),
euler(0, 1.963525369513053, 0),
new Vector3(0, 0, 0),
[
[0, 0, 7, 95, 0, 0, 0, 0, 23, 106],
[128, 239, 100, 192],
],
),
new QuestNpcModel(
NpcType.Tekker,
28,
1009,
undefined,
0,
830,
0,
0,
30,
new Vector3(-43.70983123779297, 2.5999999046325684, -52.78248596191406),
euler(0, 0.7854101478052212, 0),
new Vector3(0, 0, 0),
[
[0, 0, 7, 97, 0, 0, 0, 0, 23, 98],
[128, 239, 16, 176],
],
),
new QuestNpcModel(
NpcType.MaleMacho,
12,
1006,
undefined,
0,
800,
0,
0,
30,
new Vector3(0.33990478515625, 2.5999999046325684, -84.71995544433594),
euler(0, 0, 0),
new Vector3(0, 0, 0),
[
[0, 0, 7, 98, 0, 0, 0, 0, 23, 99],
[128, 239, 21, 48],
],
),
new QuestNpcModel(
NpcType.FemaleMacho,
5,
1008,
undefined,
0,
820,
0,
0,
30,
new Vector3(43.87113952636719, 2.5999996662139893, -74.80299377441406),
new Euler(0, -0.5645135437350027, 0, "ZXY"),
new Vector3(0, 0, 0),
[
[0, 0, 7, 99, 0, 0, 0, 0, 23, 100],
[128, 239, 25, 176],
],
),
new QuestNpcModel(
NpcType.MaleFat,
11,
1007.0010986328125,
undefined,
0,
810,
0,
0,
30,
new Vector3(75.88380432128906, 2.5999996662139893, -42.69328308105469),
new Euler(0, -1.0308508189943528, 0, "ZXY"),
new Vector3(0, 0, 0),
[
[0, 0, 7, 100, 0, 0, 0, 0, 23, 101],
[128, 239, 30, 48],
],
),
new QuestNpcModel(
NpcType.FemaleTall,
7,
1021.0010986328125,
undefined,
0,
140,
1,
0,
30,
new Vector3(16.003997802734375, 0, 5.995697021484375),
new Euler(0, -1.1781152217078317, 0, "ZXY"),
new Vector3(22.000009536743164, 0, 0),
[
[0, 0, 7, 101, 0, 0, 0, 0, 23, 102],
[128, 239, 34, 176],
],
),
new QuestNpcModel(
NpcType.Nurse,
31,
1017,
undefined,
0,
860,
0,
0,
40,
new Vector3(0.3097381591796875, 3, -105.3865966796875),
euler(0, 0, 0),
new Vector3(0, 0, 0),
[
[0, 0, 7, 102, 0, 0, 0, 0, 23, 103],
[128, 239, 39, 48],
],
),
new QuestNpcModel(
NpcType.Nurse,
31,
1018.0010986328125,
undefined,
0,
110,
1,
0,
40,
new Vector3(53.499176025390625, 0, -26.496688842773438),
euler(0, 5.497871034636549, 0),
new Vector3(18.000009536743164, 0, 0),
[
[0, 0, 7, 103, 0, 0, 0, 0, 23, 104],
[128, 239, 43, 176],
],
),
];
}
function create_default_event_dags(): Map<number, QuestEventDagModel> {
return new Map();
}
function create_default_object_code(): Segment[] {
return assemble(
`.code
0:
set_episode 0
set_floor_handler 0, 150
bb_map_designate 0, 0, 0, 0
ret
150:
leti r60, 237
leti r61, 0
leti r62, 333
leti r63, -15
p_setpos 0, r60
leti r60, 255
leti r61, 0
leti r62, 338
leti r63, -43
p_setpos 1, r60
leti r60, 222
leti r61, 0
leti r62, 322
leti r63, 25
p_setpos 2, r60
leti r60, 248
leti r61, 0
leti r62, 323
leti r63, -20
p_setpos 3, r60
ret`.split("\n"),
).object_code;
}

View File

@ -1,28 +1,29 @@
import { Disposable } from "../../../../src/core/observable/Disposable";
import { Disposer } from "../../../../src/core/observable/Disposer";
import { is_promise } from "../../../../src/core/util";
export function with_disposable<D extends Disposable, T>(
disposable: D,
f: (disposable: D) => T | Promise<T>,
f: (disposable: D) => T,
): T {
let is_promise = false;
let return_promise = false;
try {
const value = f(disposable);
if (value != undefined && "then" in value && "finally" in value) {
is_promise = true;
return (value.finally(() => disposable.dispose()) as any) as T;
if (is_promise(value)) {
return_promise = true;
return (value.finally(() => disposable.dispose()) as unknown) as T;
} else {
return value;
}
} finally {
if (!is_promise) {
if (!return_promise) {
disposable.dispose();
}
}
}
export function with_disposer<T>(f: (disposer: Disposer) => T | Promise<T>): T {
export function with_disposer<T>(f: (disposer: Disposer) => T): T {
return with_disposable(new Disposer(), f);
}

View File

@ -1,6 +1,13 @@
import * as fs from "fs";
import { InstructionSegment, SegmentType } from "../../src/core/data_formats/asm/instructions";
import { assemble } from "../../src/quest_editor/scripting/assembly";
import { parse_qst_to_quest } from "../../src/core/data_formats/parsing/quest";
import { BufferCursor } from "../../src/core/data_formats/block/cursor/BufferCursor";
import { Endianness } from "../../src/core/data_formats/block/Endianness";
import { Quest } from "../../src/core/data_formats/parsing/quest/Quest";
import { QuestModel } from "../../src/quest_editor/model/QuestModel";
import { AreaStore } from "../../src/quest_editor/stores/AreaStore";
import { convert_quest_to_model } from "../../src/quest_editor/stores/model_conversion";
export async function timeout(millis: number): Promise<void> {
return new Promise(resolve => {
@ -59,6 +66,17 @@ export function get_qst_files(dir: string): [string, string][] {
return files;
}
export function load_default_quest_model(area_store: AreaStore): QuestModel {
return convert_quest_to_model(
area_store,
load_qst_as_quest("assets/quests/defaults/default_ep_1.qst")!,
);
}
export function load_qst_as_quest(path: string): Quest | undefined {
return parse_qst_to_quest(new BufferCursor(fs.readFileSync(path), Endianness.Little))?.quest;
}
export function to_instructions(assembly: string, manual_stack?: boolean): InstructionSegment[] {
const { object_code, warnings, errors } = assemble(assembly.split("\n"), manual_stack);