From 329ca0e539d7574b2a7dd9c6c829a2be3ae9d6b4 Mon Sep 17 00:00:00 2001 From: Daan Vanden Bosch Date: Sun, 19 Jul 2020 18:11:17 +0200 Subject: [PATCH] Made low-level quest objects structurally cloneable again. --- src/core/data_formats/parsing/quest/Quest.ts | 342 ++---------------- .../quest/{get_npc_type.ts => QuestNpc.ts} | 200 +++++++++- .../data_formats/parsing/quest/QuestObject.ts | 129 +++++++ .../data_formats/parsing/quest/index.test.ts | 18 +- src/core/data_formats/parsing/quest/index.ts | 79 ++-- .../model/QuestEntityModel.test.ts | 4 +- src/quest_editor/model/QuestEntityModel.ts | 32 +- src/quest_editor/model/QuestNpcModel.ts | 46 ++- src/quest_editor/model/QuestObjectModel.ts | 40 +- .../rendering/QuestEntityControls.ts | 13 +- src/quest_editor/stores/model_conversion.ts | 12 +- 11 files changed, 504 insertions(+), 411 deletions(-) rename src/core/data_formats/parsing/quest/{get_npc_type.ts => QuestNpc.ts} (61%) create mode 100644 src/core/data_formats/parsing/quest/QuestObject.ts diff --git a/src/core/data_formats/parsing/quest/Quest.ts b/src/core/data_formats/parsing/quest/Quest.ts index 1d34a9f4..6237b8d8 100644 --- a/src/core/data_formats/parsing/quest/Quest.ts +++ b/src/core/data_formats/parsing/quest/Quest.ts @@ -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, - ) {} -} +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; +}; export type EntityTypeData = NpcTypeData | ObjectTypeData; export type EntityType = NpcType | ObjectType; -export interface QuestEntity { - area_id: number; - readonly data: ArrayBufferBlock; - type: Type; - section_id: number; - position: Vec3; - rotation: Vec3; -} - -export class QuestNpc implements QuestEntity { - 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 { - 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); } diff --git a/src/core/data_formats/parsing/quest/get_npc_type.ts b/src/core/data_formats/parsing/quest/QuestNpc.ts similarity index 61% rename from src/core/data_formats/parsing/quest/get_npc_type.ts rename to src/core/data_formats/parsing/quest/QuestNpc.ts index 33f05022..097aaf3b 100644 --- a/src/core/data_formats/parsing/quest/get_npc_type.ts +++ b/src/core/data_formats/parsing/quest/QuestNpc.ts @@ -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, + ); +} diff --git a/src/core/data_formats/parsing/quest/QuestObject.ts b/src/core/data_formats/parsing/quest/QuestObject.ts new file mode 100644 index 00000000..68583a9a --- /dev/null +++ b/src/core/data_formats/parsing/quest/QuestObject.ts @@ -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; + } +} diff --git a/src/core/data_formats/parsing/quest/index.test.ts b/src/core/data_formats/parsing/quest/index.test.ts index 241b837a..a741502f 100644 --- a/src/core/data_formats/parsing/quest/index.test.ts +++ b/src/core/data_formats/parsing/quest/index.test.ts @@ -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); diff --git a/src/core/data_formats/parsing/quest/index.ts b/src/core/data_formats/parsing/quest/index.ts index be25da7c..f1246c20 100644 --- a/src/core/data_formats/parsing/quest/index.ts +++ b/src/core/data_formats/parsing/quest/index.ts @@ -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 })); } diff --git a/src/quest_editor/model/QuestEntityModel.test.ts b/src/quest_editor/model/QuestEntityModel.test.ts index 6976199e..6c881313 100644 --- a/src/quest_editor/model/QuestEntityModel.test.ts +++ b/src/quest_editor/model/QuestEntityModel.test.ts @@ -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; diff --git a/src/quest_editor/model/QuestEntityModel.ts b/src/quest_editor/model/QuestEntityModel.ts index c25b58c9..8e7fc8a4 100644 --- a/src/quest_editor/model/QuestEntityModel.ts +++ b/src/quest_editor/model/QuestEntityModel.ts @@ -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 = QuestEntity + Entity extends QuestEntity = QuestEntity > { private readonly _section_id: WritableProperty; private readonly _section: WritableProperty = 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 { diff --git a/src/quest_editor/model/QuestNpcModel.ts b/src/quest_editor/model/QuestNpcModel.ts index ebcd136f..84f5f4fd 100644 --- a/src/quest_editor/model/QuestNpcModel.ts +++ b/src/quest_editor/model/QuestNpcModel.ts @@ -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 { + get type(): NpcType { + return get_npc_type(this.entity); + } + private readonly _wave: WritableProperty; readonly wave: Property; @@ -23,9 +39,33 @@ export class QuestNpcModel extends QuestEntityModel { 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); + } } diff --git a/src/quest_editor/model/QuestObjectModel.ts b/src/quest_editor/model/QuestObjectModel.ts index a4bf0ee5..b506cf1b 100644 --- a/src/quest_editor/model/QuestObjectModel.ts +++ b/src/quest_editor/model/QuestObjectModel.ts @@ -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 { + 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); + } } diff --git a/src/quest_editor/rendering/QuestEntityControls.ts b/src/quest_editor/rendering/QuestEntityControls.ts index 077c7795..5fee5033 100644 --- a/src/quest_editor/rendering/QuestEntityControls.ts +++ b/src/quest_editor/rendering/QuestEntityControls.ts @@ -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( diff --git a/src/quest_editor/stores/model_conversion.ts b/src/quest_editor/stores/model_conversion.ts index 3711670a..499e004a 100644 --- a/src/quest_editor/stores/model_conversion.ts +++ b/src/quest_editor/stores/model_conversion.ts @@ -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, 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); }