Made low-level quest objects structurally cloneable again.

This commit is contained in:
Daan Vanden Bosch 2020-07-19 18:11:17 +02:00
parent 0704fc194c
commit 329ca0e539
11 changed files with 504 additions and 411 deletions

View File

@ -1,327 +1,35 @@
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 { object_data, ObjectType, ObjectTypeData } from "./object_types";
import { DatEvent, DatUnknown } 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";
import { QuestNpc } from "./QuestNpc";
import { QuestObject } from "./QuestObject";
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 Quest = {
id: number;
language: number;
name: string;
short_description: string;
long_description: string;
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 QuestEntity = QuestNpc | QuestObject;
export type QuestEvent = DatEvent;
@ -333,6 +41,10 @@ export function is_npc_type(entity_type: EntityType): entity_type is NpcType {
return NpcType[entity_type] != undefined;
}
export function is_object_type(entity_type: EntityType): entity_type is ObjectType {
return ObjectType[entity_type] != undefined;
}
export function entity_data(type: EntityType): EntityTypeData {
return npc_data(type as NpcType) ?? object_data(type as ObjectType);
}

View File

@ -1,13 +1,169 @@
import { NpcType } from "./npc_types";
import { npc_data, NpcType } from "./npc_types";
import { Vec3 } from "../../vector";
import { Episode } from "./Episode";
import { NPC_BYTE_SIZE } from "./dat";
import { assert } from "../../../util";
const DEFAULT_SCALE: Vec3 = Object.freeze({ x: 1, y: 1, z: 1 });
export type QuestNpc = {
episode: Episode;
area_id: number;
readonly data: ArrayBuffer;
readonly view: DataView;
};
export function create_quest_npc(type: NpcType, area_id: number, wave: number): QuestNpc {
const data = new ArrayBuffer(NPC_BYTE_SIZE);
const npc: QuestNpc = {
episode: Episode.I,
area_id,
data,
view: new DataView(data),
};
// Set scale before type, because set_npc_type will change it.
set_npc_scale(npc, DEFAULT_SCALE);
set_npc_type(npc, type);
// Set area_id after type, because you might want to overwrite the area_id that type has
// determined.
npc.area_id = area_id;
set_npc_wave(npc, wave);
set_npc_wave_2(npc, wave);
return npc;
}
export function data_to_quest_npc(episode: Episode, area_id: number, data: ArrayBuffer): QuestNpc {
assert(
data.byteLength === NPC_BYTE_SIZE,
() => `Data byteLength should be ${NPC_BYTE_SIZE} but was ${data.byteLength}.`,
);
return {
episode,
area_id,
data,
view: new DataView(data),
};
}
//
// Simple properties that directly map to a part of the data block.
//
export function get_npc_type_id(npc: QuestNpc): number {
return npc.view.getUint16(0, true);
}
export function set_npc_type_id(npc: QuestNpc, type_id: number): void {
npc.view.setUint16(0, type_id, true);
}
export function get_npc_section_id(npc: QuestNpc): number {
return npc.view.getUint16(12, true);
}
export function set_npc_section_id(npc: QuestNpc, section_id: number): void {
npc.view.setUint16(12, section_id, true);
}
export function get_npc_wave(npc: QuestNpc): number {
return npc.view.getUint16(14, true);
}
export function set_npc_wave(npc: QuestNpc, wave: number): void {
npc.view.setUint16(14, wave, true);
}
export function get_npc_wave_2(npc: QuestNpc): number {
return npc.view.getUint32(16, true);
}
export function set_npc_wave_2(npc: QuestNpc, wave_2: number): void {
npc.view.setUint32(16, wave_2, true);
}
/**
* Section-relative position.
*/
export function get_npc_position(npc: QuestNpc): Vec3 {
return {
x: npc.view.getFloat32(20, true),
y: npc.view.getFloat32(24, true),
z: npc.view.getFloat32(28, true),
};
}
export function set_npc_position(npc: QuestNpc, position: Vec3): void {
npc.view.setFloat32(20, position.x, true);
npc.view.setFloat32(24, position.y, true);
npc.view.setFloat32(28, position.z, true);
}
export function get_npc_rotation(npc: QuestNpc): Vec3 {
return {
x: (npc.view.getInt32(32, true) / 0xffff) * 2 * Math.PI,
y: (npc.view.getInt32(36, true) / 0xffff) * 2 * Math.PI,
z: (npc.view.getInt32(40, true) / 0xffff) * 2 * Math.PI,
};
}
export function set_npc_rotation(npc: QuestNpc, rotation: Vec3): void {
npc.view.setInt32(32, Math.round((rotation.x / (2 * Math.PI)) * 0xffff), true);
npc.view.setInt32(36, Math.round((rotation.y / (2 * Math.PI)) * 0xffff), true);
npc.view.setInt32(40, Math.round((rotation.z / (2 * Math.PI)) * 0xffff), true);
}
/**
* Seemingly 3 floats, not sure what they represent.
* The y component is used to help determine what the NpcType is.
*/
export function get_npc_scale(npc: QuestNpc): Vec3 {
return {
x: npc.view.getFloat32(44, true),
y: npc.view.getFloat32(48, true),
z: npc.view.getFloat32(52, true),
};
}
export function set_npc_scale(npc: QuestNpc, scale: Vec3): void {
npc.view.setFloat32(44, scale.x, true);
npc.view.setFloat32(48, scale.y, true);
npc.view.setFloat32(52, scale.z, true);
}
export function get_npc_id(npc: QuestNpc): number {
return npc.view.getFloat32(56, true);
}
/**
* Only seems to be valid for non-enemies.
*/
export function get_npc_script_label(npc: QuestNpc): number {
return Math.round(npc.view.getFloat32(60, true));
}
export function get_npc_skin(npc: QuestNpc): number {
return npc.view.getUint32(64, true);
}
export function set_npc_skin(npc: QuestNpc, skin: number): void {
npc.view.setUint32(64, skin, true);
}
//
// Complex properties that use multiple parts of the data block and possible other properties.
//
// 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 {
export function get_npc_type(npc: QuestNpc): NpcType {
const episode = npc.episode;
const type_id = get_npc_type_id(npc);
const regular = is_npc_regular(npc);
const skin = get_npc_skin(npc);
const area_id = npc.area_id;
switch (`${type_id}, ${skin % 3}, ${episode}`) {
case `${0x044}, 0, 1`:
return NpcType.Booma;
@ -279,3 +435,31 @@ export function get_npc_type(
return NpcType.Unknown;
}
export function set_npc_type(npc: QuestNpc, type: NpcType): void {
const data = npc_data(type);
if (data.episode != undefined) {
npc.episode = data.episode;
}
set_npc_type_id(npc, data.type_id ?? 0);
set_npc_regular(npc, data.regular ?? true);
set_npc_skin(npc, data.skin ?? 0);
if (data.area_ids.length > 0 && !data.area_ids.includes(npc.area_id)) {
npc.area_id = data.area_ids[0];
}
}
export function is_npc_regular(npc: QuestNpc): boolean {
return Math.abs(npc.view.getFloat32(48, true) - 1) > 0.00001;
}
export function set_npc_regular(npc: QuestNpc, regular: boolean): void {
npc.view.setInt32(
48,
(npc.view.getInt32(48, true) & ~0x800000) | (regular ? 0 : 0x800000),
true,
);
}

View File

@ -0,0 +1,129 @@
import { id_to_object_type, object_data, ObjectType } from "./object_types";
import { Vec3 } from "../../vector";
import { OBJECT_BYTE_SIZE } from "./dat";
import { assert } from "../../../util";
export type QuestObject = {
area_id: number;
readonly data: ArrayBuffer;
readonly view: DataView;
};
export function create_quest_object(type: ObjectType, area_id: number): QuestObject {
const data = new ArrayBuffer(OBJECT_BYTE_SIZE);
const obj: QuestObject = {
area_id,
data,
view: new DataView(data),
};
set_object_type(obj, type);
return obj;
}
export function data_to_quest_object(area_id: number, data: ArrayBuffer): QuestObject {
assert(
data.byteLength === OBJECT_BYTE_SIZE,
() => `Data byteLength should be ${OBJECT_BYTE_SIZE} but was ${data.byteLength}.`,
);
return {
area_id,
data,
view: new DataView(data),
};
}
//
// Simple properties that directly map to a part of the data block.
//
export function get_object_type_id(object: QuestObject): number {
return object.view.getUint16(0, true);
}
export function set_object_type_id(object: QuestObject, type_id: number): void {
object.view.setUint16(0, type_id, true);
}
export function get_object_id(object: QuestObject): number {
return object.view.getUint16(8, true);
}
export function get_object_group_id(object: QuestObject): number {
return object.view.getUint16(10, true);
}
export function get_object_section_id(object: QuestObject): number {
return object.view.getUint16(12, true);
}
export function set_object_section_id(object: QuestObject, section_id: number): void {
object.view.setUint16(12, section_id, true);
}
/**
* Section-relative position.
*/
export function get_object_position(object: QuestObject): Vec3 {
return {
x: object.view.getFloat32(16, true),
y: object.view.getFloat32(20, true),
z: object.view.getFloat32(24, true),
};
}
export function set_object_position(object: QuestObject, position: Vec3): void {
object.view.setFloat32(16, position.x, true);
object.view.setFloat32(20, position.y, true);
object.view.setFloat32(24, position.z, true);
}
export function get_object_rotation(object: QuestObject): Vec3 {
return {
x: (object.view.getInt32(28, true) / 0xffff) * 2 * Math.PI,
y: (object.view.getInt32(32, true) / 0xffff) * 2 * Math.PI,
z: (object.view.getInt32(36, true) / 0xffff) * 2 * Math.PI,
};
}
export function set_object_rotation(object: QuestObject, rotation: Vec3): void {
object.view.setInt32(28, Math.round((rotation.x / (2 * Math.PI)) * 0xffff), true);
object.view.setInt32(32, Math.round((rotation.y / (2 * Math.PI)) * 0xffff), true);
object.view.setInt32(36, Math.round((rotation.z / (2 * Math.PI)) * 0xffff), true);
}
//
// Complex properties that use multiple parts of the data block and possible other properties.
//
export function get_object_type(object: QuestObject): ObjectType {
return id_to_object_type(get_object_type_id(object));
}
export function set_object_type(object: QuestObject, type: ObjectType): void {
set_object_type_id(object, object_data(type).type_id ?? 0);
}
export function get_object_script_label(object: QuestObject): number | undefined {
switch (get_object_type(object)) {
case ObjectType.ScriptCollision:
case ObjectType.ForestConsole:
case ObjectType.TalkLinkToSupport:
return object.view.getUint32(52, true);
case ObjectType.RicoMessagePod:
return object.view.getUint32(56, true);
default:
return undefined;
}
}
export function get_object_script_label_2(object: QuestObject): number | undefined {
switch (get_object_type(object)) {
case ObjectType.RicoMessagePod:
return object.view.getUint32(60, true);
default:
return undefined;
}
}

View File

@ -11,6 +11,8 @@ import {
SegmentType,
StringSegment,
} from "../../asm/instructions";
import { get_object_position, get_object_section_id, get_object_type } from "./QuestObject";
import { get_npc_position, get_npc_section_id, get_npc_type } from "./QuestNpc";
test("parse Towards the Future", () => {
const buffer = readFileSync("test/resources/quest118_e.qst");
@ -24,8 +26,8 @@ test("parse Towards the Future", () => {
);
expect(quest.episode).toBe(1);
expect(quest.objects.length).toBe(277);
expect(quest.objects[0].type).toBe(ObjectType.MenuActivation);
expect(quest.objects[4].type).toBe(ObjectType.PlayerSet);
expect(get_object_type(quest.objects[0])).toBe(ObjectType.MenuActivation);
expect(get_object_type(quest.objects[4])).toBe(ObjectType.PlayerSet);
expect(quest.npcs.length).toBe(216);
expect(quest.map_designations).toEqual(
new Map([
@ -89,9 +91,9 @@ function round_trip_test(path: string, file_name: string, contents: Buffer): voi
const orig_obj = orig_quest.objects[i];
const test_obj = test_quest.objects[i];
expect(test_obj.area_id).toBe(orig_obj.area_id);
expect(test_obj.section_id).toBe(orig_obj.section_id);
expect(test_obj.position).toEqual(orig_obj.position);
expect(test_obj.type).toBe(orig_obj.type);
expect(get_object_section_id(test_obj)).toBe(get_object_section_id(orig_obj));
expect(get_object_position(test_obj)).toEqual(get_object_position(orig_obj));
expect(get_object_type(test_obj)).toBe(get_object_type(orig_obj));
}
expect(test_quest.npcs.length).toBe(orig_quest.npcs.length);
@ -100,9 +102,9 @@ function round_trip_test(path: string, file_name: string, contents: Buffer): voi
const orig_npc = orig_quest.npcs[i];
const test_npc = test_quest.npcs[i];
expect(test_npc.area_id).toBe(orig_npc.area_id);
expect(test_npc.section_id).toBe(orig_npc.section_id);
expect(test_npc.position).toEqual(orig_npc.position);
expect(test_npc.type).toBe(orig_npc.type);
expect(get_npc_section_id(test_npc)).toBe(get_npc_section_id(orig_npc));
expect(get_npc_position(test_npc)).toEqual(get_npc_position(orig_npc));
expect(get_npc_type(test_npc)).toBe(get_npc_type(orig_npc));
}
expect(test_quest.map_designations).toEqual(orig_quest.map_designations);

View File

@ -8,7 +8,7 @@ import { ResizableBlockCursor } from "../../block/cursor/ResizableBlockCursor";
import { Endianness } from "../../block/Endianness";
import { parse_bin, write_bin } from "./bin";
import { DatEntity, parse_dat, write_dat } from "./dat";
import { Quest, QuestNpc, QuestObject } from "./Quest";
import { Quest, QuestEntity } from "./Quest";
import { Episode } from "./Episode";
import { parse_qst, QstContainedFile, write_qst } from "./qst";
import { LogManager } from "../../../Logger";
@ -17,7 +17,13 @@ import { get_map_designations } from "../../asm/data_flow_analysis/get_map_desig
import { basename } from "../../../util";
import { version_to_bin_format } from "./BinFormat";
import { Version } from "./Version";
import { ArrayBufferBlock } from "../../block/ArrayBufferBlock";
import { data_to_quest_npc, get_npc_script_label, QuestNpc } from "./QuestNpc";
import {
data_to_quest_object,
get_object_script_label,
get_object_script_label_2,
QuestObject,
} from "./QuestObject";
const logger = LogManager.get("core/data_formats/parsing/quest");
@ -32,9 +38,9 @@ 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);
const objects = dat.objs.map(({ area_id, data }) => data_to_quest_object(area_id, data));
// Initialize NPCs with random episode and correct it later.
const npcs = parse_npc_data(Episode.I, dat.npcs);
const npcs = dat.npcs.map(({ area_id, data }) => data_to_quest_npc(Episode.I, area_id, data));
// Extract episode and map designations from object code.
let episode = Episode.I;
@ -77,21 +83,21 @@ export function parse_bin_dat_to_quest(
logger.warn("File contains no instruction labels.");
}
return new Quest(
bin.quest_id,
bin.language,
bin.quest_name,
bin.short_description,
bin.long_description,
return {
id: bin.quest_id,
language: bin.language,
name: bin.quest_name,
short_description: bin.short_description,
long_description: bin.long_description,
episode,
objects,
npcs,
dat.events,
dat.unknowns,
events: dat.events,
dat_unknowns: dat.unknowns,
object_code,
bin.shop_items,
shop_items: bin.shop_items,
map_designations,
);
};
}
export function parse_qst_to_quest(
@ -144,8 +150,8 @@ export function write_quest_qst(
online: boolean,
): ArrayBuffer {
const dat = write_dat({
objs: objects_to_dat_data(quest.objects),
npcs: npcs_to_dat_data(quest.npcs),
objs: entities_to_dat_data(quest.objects),
npcs: entities_to_dat_data(quest.npcs),
events: quest.events,
unknowns: quest.dat_unknowns,
});
@ -226,13 +232,13 @@ function extract_script_entry_points(
const entry_points = new Set([0]);
for (const obj of objects) {
const entry_point = obj.script_label;
const entry_point = get_object_script_label(obj);
if (entry_point != undefined) {
entry_points.add(entry_point);
}
const entry_point_2 = obj.script_label_2;
const entry_point_2 = get_object_script_label_2(obj);
if (entry_point_2 != undefined) {
entry_points.add(entry_point_2);
@ -240,43 +246,12 @@ function extract_script_entry_points(
}
for (const npc of npcs) {
entry_points.add(npc.script_label);
entry_points.add(get_npc_script_label(npc));
}
return [...entry_points];
}
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),
),
);
}
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,
data: object.data.backing_buffer,
}));
}
function npcs_to_dat_data(npcs: readonly QuestNpc[]): DatEntity[] {
return npcs.map(npc => ({
area_id: npc.area_id,
data: npc.data.backing_buffer,
}));
function entities_to_dat_data(entities: readonly QuestEntity[]): DatEntity[] {
return entities.map(({ area_id, data }) => ({ area_id, data }));
}

View File

@ -8,7 +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";
import { create_quest_npc } from "../../core/data_formats/parsing/quest/QuestNpc";
const area_store = new AreaStore(new AreaAssetLoader(new StubHttpClient()));
@ -45,7 +45,7 @@ test("After changing section, world position should change accordingly.", () =>
function create_entity(): QuestEntityModel {
const entity = new QuestNpcModel(
QuestNpc.create(NpcType.AlRappy, area_store.get_area(Episode.I, 0).id, 0),
create_quest_npc(NpcType.AlRappy, area_store.get_area(Episode.I, 0).id, 0),
);
entity.set_position(new Vector3(5, 5, 5));
return entity;

View File

@ -7,6 +7,7 @@ import { Euler, Quaternion, Vector3 } from "three";
import { floor_mod } from "../../core/math";
import { euler, euler_from_quat } from "./euler";
import { vec3_to_threejs } from "../../core/rendering/conversion";
import { Vec3 } from "../../core/data_formats/vector";
// These quaternions are used as temporary variables to avoid memory allocation.
const q1 = new Quaternion();
@ -14,7 +15,7 @@ const q2 = new Quaternion();
export abstract class QuestEntityModel<
Type extends EntityType = EntityType,
Entity extends QuestEntity<Type> = QuestEntity<Type>
Entity extends QuestEntity = QuestEntity
> {
private readonly _section_id: WritableProperty<number>;
private readonly _section: WritableProperty<SectionModel | undefined> = property(undefined);
@ -29,9 +30,7 @@ export abstract class QuestEntityModel<
*/
readonly entity: Entity;
get type(): Type {
return this.entity.type;
}
abstract readonly type: Type;
get area_id(): number {
return this.entity.area_id;
@ -60,10 +59,10 @@ export abstract class QuestEntityModel<
this.section = this._section;
this._section_id = property(entity.section_id);
this._section_id = property(this.get_entity_section_id());
this.section_id = this._section_id;
const position = vec3_to_threejs(entity.position);
const position = vec3_to_threejs(this.get_entity_position());
this._position = property(position);
this.position = this._position;
@ -71,7 +70,7 @@ export abstract class QuestEntityModel<
this._world_position = property(position);
this.world_position = this._world_position;
const { x: rot_x, y: rot_y, z: rot_z } = entity.rotation;
const { x: rot_x, y: rot_y, z: rot_z } = this.get_entity_rotation();
const rotation = euler(rot_x, rot_y, rot_z);
this._rotation = property(rotation);
@ -86,7 +85,7 @@ export abstract class QuestEntityModel<
throw new Error(`Quest entities can't be moved across areas.`);
}
this.entity.section_id = section.id;
this.set_entity_section_id(section.id);
this._section.val = section;
this._section_id.val = section.id;
@ -98,7 +97,7 @@ export abstract class QuestEntityModel<
}
set_position(pos: Vector3): this {
this.entity.position = pos;
this.set_entity_position(pos);
this._position.val = pos;
@ -120,7 +119,7 @@ export abstract class QuestEntityModel<
? pos.clone().sub(section.position).applyEuler(section.inverse_rotation)
: pos;
this.entity.position = rel_pos;
this.set_entity_position(rel_pos);
this._position.val = rel_pos;
return this;
@ -129,7 +128,7 @@ export abstract class QuestEntityModel<
set_rotation(rot: Euler): this {
floor_mod_euler(rot);
this.entity.rotation = rot;
this.set_entity_rotation(rot);
this._rotation.val = rot;
@ -165,11 +164,20 @@ export abstract class QuestEntityModel<
rel_rot = rot;
}
this.entity.rotation = rel_rot;
this.set_entity_rotation(rel_rot);
this._rotation.val = rel_rot;
return this;
}
protected abstract get_entity_section_id(): number;
protected abstract set_entity_section_id(section_id: number): void;
protected abstract get_entity_position(): Vec3;
protected abstract set_entity_position(position: Vec3): void;
protected abstract get_entity_rotation(): Vec3;
protected abstract set_entity_rotation(rotation: Vec3): void;
}
function floor_mod_euler(euler: Euler): Euler {

View File

@ -5,9 +5,25 @@ import { Property } from "../../core/observable/property/Property";
import { defined } from "../../core/util";
import { property } from "../../core/observable";
import { WaveModel } from "./WaveModel";
import { QuestNpc } from "../../core/data_formats/parsing/quest/Quest";
import {
get_npc_position,
get_npc_rotation,
get_npc_section_id,
get_npc_type,
QuestNpc,
set_npc_position,
set_npc_rotation,
set_npc_section_id,
set_npc_wave,
set_npc_wave_2,
} from "../../core/data_formats/parsing/quest/QuestNpc";
import { Vec3 } from "../../core/data_formats/vector";
export class QuestNpcModel extends QuestEntityModel<NpcType, QuestNpc> {
get type(): NpcType {
return get_npc_type(this.entity);
}
private readonly _wave: WritableProperty<WaveModel | undefined>;
readonly wave: Property<WaveModel | undefined>;
@ -23,9 +39,33 @@ export class QuestNpcModel extends QuestEntityModel<NpcType, QuestNpc> {
set_wave(wave?: WaveModel): this {
const wave_id = wave?.id?.val ?? 0;
this.entity.wave = wave_id;
this.entity.wave_2 = wave_id;
set_npc_wave(this.entity, wave_id);
set_npc_wave_2(this.entity, wave_id);
this._wave.val = wave;
return this;
}
protected get_entity_section_id(): number {
return get_npc_section_id(this.entity);
}
protected set_entity_section_id(section_id: number): void {
set_npc_section_id(this.entity, section_id);
}
protected get_entity_position(): Vec3 {
return get_npc_position(this.entity);
}
protected set_entity_position(position: Vec3): void {
set_npc_position(this.entity, position);
}
protected get_entity_rotation(): Vec3 {
return get_npc_rotation(this.entity);
}
protected set_entity_rotation(rotation: Vec3): void {
set_npc_rotation(this.entity, rotation);
}
}

View File

@ -1,12 +1,50 @@
import { QuestEntityModel } from "./QuestEntityModel";
import { ObjectType } from "../../core/data_formats/parsing/quest/object_types";
import { QuestObject } from "../../core/data_formats/parsing/quest/Quest";
import { defined } from "../../core/util";
import {
get_object_position,
get_object_rotation,
get_object_section_id,
get_object_type,
QuestObject,
set_object_position,
set_object_rotation,
set_object_section_id,
} from "../../core/data_formats/parsing/quest/QuestObject";
import { Vec3 } from "../../core/data_formats/vector";
export class QuestObjectModel extends QuestEntityModel<ObjectType, QuestObject> {
get type(): ObjectType {
return get_object_type(this.entity);
}
constructor(object: QuestObject) {
defined(object, "object");
super(object);
}
protected get_entity_section_id(): number {
return get_object_section_id(this.entity);
}
protected set_entity_section_id(section_id: number): void {
set_object_section_id(this.entity, section_id);
}
protected get_entity_position(): Vec3 {
return get_object_position(this.entity);
}
protected set_entity_position(position: Vec3): void {
set_object_position(this.entity, position);
}
protected get_entity_rotation(): Vec3 {
return get_object_rotation(this.entity);
}
protected set_entity_rotation(rotation: Vec3): void {
set_object_rotation(this.entity, rotation);
}
}

View File

@ -7,12 +7,7 @@ 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,
QuestNpc,
QuestObject,
} from "../../core/data_formats/parsing/quest/Quest";
import { EntityType, is_npc_type } from "../../core/data_formats/parsing/quest/Quest";
import {
add_entity_dnd_listener,
EntityDragEvent,
@ -27,6 +22,8 @@ import { RotateEntityAction } from "../actions/RotateEntityAction";
import { RemoveEntityAction } from "../actions/RemoveEntityAction";
import { TranslateEntityAction } from "../actions/TranslateEntityAction";
import { Object3D } from "three/src/core/Object3D";
import { create_quest_npc } from "../../core/data_formats/parsing/quest/QuestNpc";
import { create_quest_object } from "../../core/data_formats/parsing/quest/QuestObject";
const ZERO_VECTOR = Object.freeze(new Vector3(0, 0, 0));
const UP_VECTOR = Object.freeze(new Vector3(0, 1, 0));
@ -643,11 +640,11 @@ class CreationState implements State {
const wave = quest_editor_store.selected_wave.val;
this.entity = new QuestNpcModel(
QuestNpc.create(evt.entity_type, area.id, wave?.id.val ?? 0),
create_quest_npc(evt.entity_type, area.id, wave?.id.val ?? 0),
wave,
);
} else {
this.entity = new QuestObjectModel(QuestObject.create(evt.entity_type, area.id));
this.entity = new QuestObjectModel(create_quest_object(evt.entity_type, area.id));
}
translate_entity_horizontally(

View File

@ -14,11 +14,16 @@ import {
QuestEventActionUnlockModel,
} from "../model/QuestEventActionModel";
import { QuestEventDagModel } from "../model/QuestEventDagModel";
import { Quest, QuestEvent, QuestNpc } from "../../core/data_formats/parsing/quest/Quest";
import { Quest, QuestEvent } 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 { WaveModel } from "../model/WaveModel";
import {
get_npc_section_id,
get_npc_wave,
QuestNpc,
} from "../../core/data_formats/parsing/quest/QuestNpc";
const logger = LogManager.get("quest_editor/stores/model_conversion");
@ -44,8 +49,11 @@ export function convert_quest_to_model(area_store: AreaStore, quest: Quest): Que
}
function convert_npc_to_model(wave_cache: Map<string, WaveModel>, npc: QuestNpc): QuestNpcModel {
const wave_id = get_npc_wave(npc);
const wave =
npc.wave === 0 ? undefined : get_wave(wave_cache, npc.area_id, npc.section_id, npc.wave);
wave_id === 0
? undefined
: get_wave(wave_cache, npc.area_id, get_npc_section_id(npc), wave_id);
return new QuestNpcModel(npc, wave);
}