Quest events are now grouped in DAGs.

This commit is contained in:
Daan Vanden Bosch 2019-10-29 15:22:46 +01:00
parent dff39bd194
commit 5f1b3d5ff1
7 changed files with 291 additions and 141 deletions

View File

@ -1,7 +1,7 @@
import { ResizableWidget } from "../../core/gui/ResizableWidget";
import { bind_children_to, el, icon, Icon } from "../../core/gui/dom";
import { quest_editor_store } from "../stores/QuestEditorStore";
import { QuestEventChainModel } from "../model/QuestEventChainModel";
import { QuestEventDagModel } from "../model/QuestEventDagModel";
import { Disposer } from "../../core/observable/Disposer";
import { NumberInput } from "../../core/gui/NumberInput";
import "./EventsView.css";
@ -34,18 +34,18 @@ export class EventsView extends ResizableWidget {
this.quest_disposer.add(
bind_children_to(
this.element,
quest.event_chains.filtered(chain => chain.events.get(0).area_id === area.id),
quest.event_dags.filtered(dag => dag.root_events.get(0).area_id === area.id),
this.create_chain_element,
),
);
}
};
private create_chain_element = (chain: QuestEventChainModel): [HTMLElement, Disposable] => {
private create_chain_element = (dag: QuestEventDagModel): [HTMLElement, Disposable] => {
const disposer = new Disposer();
const element = el.div(
{ class: "quest_editor_EventsView_chain" },
...chain.events.val.map(event =>
...dag.events.map(event =>
el.div(
{ class: "quest_editor_EventsView_event" },
el.table(

View File

@ -1,15 +0,0 @@
import { QuestEventModel } from "./QuestEventModel";
import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { list_property } from "../../core/observable";
export class QuestEventChainModel {
private readonly _events: WritableListProperty<QuestEventModel>;
readonly events: ListProperty<QuestEventModel>;
constructor(events: QuestEventModel[]) {
this._events = list_property(undefined, ...events);
this.events = this._events;
}
}

View File

@ -0,0 +1,45 @@
import { QuestEventModel } from "./QuestEventModel";
import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { list_property } from "../../core/observable";
export type QuestEventDagModelMeta = {
parents: QuestEventModel[];
children: QuestEventModel[];
};
/**
* Events can call each other and form a directed acyclic graph (DAG) this way.
*/
export class QuestEventDagModel {
private readonly _root_events: WritableListProperty<QuestEventModel>;
private meta: Map<QuestEventModel, QuestEventDagModelMeta>;
readonly events: QuestEventModel[];
/**
* The root nodes of the DAG. These events are not called from any other events.
*/
readonly root_events: ListProperty<QuestEventModel>;
constructor(
events: QuestEventModel[],
root_events: QuestEventModel[],
meta: Map<QuestEventModel, QuestEventDagModelMeta>,
) {
this.events = events;
this._root_events = list_property(undefined, ...root_events);
this.root_events = this._root_events;
this.meta = meta;
}
get_parents(event: QuestEventModel): readonly QuestEventModel[] {
const meta = this.meta.get(event);
return meta ? meta.parents : [];
}
get_children(event: QuestEventModel): readonly QuestEventModel[] {
const meta = this.meta.get(event);
return meta ? meta.children : [];
}
}

View File

@ -1,11 +1,16 @@
import { QuestEventActionModel } from "./QuestEventActionModel";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { list_property } from "../../core/observable";
import { ListProperty } from "../../core/observable/property/list/ListProperty";
export class QuestEventModel {
private readonly _actions: WritableListProperty<QuestEventActionModel> = list_property();
readonly id: number;
readonly section_id: number;
readonly wave: number;
readonly delay: number;
readonly actions: readonly QuestEventActionModel[];
readonly actions: ListProperty<QuestEventActionModel> = this._actions;
readonly area_id: number;
readonly unknown: number;
@ -14,7 +19,6 @@ export class QuestEventModel {
section_id: number,
wave: number,
delay: number,
actions: readonly QuestEventActionModel[],
area_id: number,
unknown: number,
) {
@ -22,7 +26,6 @@ export class QuestEventModel {
if (!Number.isInteger(section_id)) throw new Error("section_id should be an integer.");
if (!Number.isInteger(wave)) throw new Error("wave should be an integer.");
if (!Number.isInteger(delay)) throw new Error("delay should be an integer.");
if (!Array.isArray(actions)) throw new Error("actions should be an array.");
if (!Number.isInteger(area_id)) throw new Error("area_id should be an integer.");
if (!Number.isInteger(unknown)) throw new Error("unknown should be an integer.");
@ -30,8 +33,11 @@ export class QuestEventModel {
this.section_id = section_id;
this.wave = wave;
this.delay = delay;
this.actions = actions;
this.area_id = area_id;
this.unknown = unknown;
}
add_action(action: QuestEventActionModel): void {
this._actions.push(action);
}
}

View File

@ -13,7 +13,7 @@ import { ListProperty } from "../../core/observable/property/list/ListProperty";
import { WritableListProperty } from "../../core/observable/property/list/WritableListProperty";
import { QuestEntityModel } from "./QuestEntityModel";
import { entity_type_to_string } from "../../core/data_formats/parsing/quest/entities";
import { QuestEventChainModel } from "./QuestEventChainModel";
import { QuestEventDagModel } from "./QuestEventDagModel";
const logger = Logger.get("quest_editor/model/QuestModel");
@ -27,7 +27,7 @@ export class QuestModel {
private readonly _area_variants: WritableListProperty<AreaVariantModel> = list_property();
private readonly _objects: WritableListProperty<QuestObjectModel>;
private readonly _npcs: WritableListProperty<QuestNpcModel>;
private readonly _event_chains: WritableListProperty<QuestEventChainModel>;
private readonly _event_dags: WritableListProperty<QuestEventDagModel>;
readonly id: Property<number> = this._id;
@ -60,7 +60,7 @@ export class QuestModel {
readonly npcs: ListProperty<QuestNpcModel>;
readonly event_chains: ListProperty<QuestEventChainModel>;
readonly event_dags: ListProperty<QuestEventDagModel>;
/**
* (Partial) raw DAT data that can't be parsed yet by Phantasmal.
@ -81,7 +81,7 @@ export class QuestModel {
map_designations: Map<number, number>,
objects: readonly QuestObjectModel[],
npcs: readonly QuestNpcModel[],
event_chains: readonly QuestEventChainModel[],
event_dags: readonly QuestEventDagModel[],
dat_unknowns: readonly DatUnknown[],
object_code: readonly Segment[],
shop_items: readonly number[],
@ -90,7 +90,7 @@ export class QuestModel {
if (!map_designations) throw new Error("map_designations is required.");
if (!Array.isArray(objects)) throw new Error("objs is required.");
if (!Array.isArray(npcs)) throw new Error("npcs is required.");
if (!Array.isArray(event_chains)) throw new Error("event_chains is required.");
if (!Array.isArray(event_dags)) throw new Error("event_dags is required.");
if (!Array.isArray(dat_unknowns)) throw new Error("dat_unknowns is required.");
if (!Array.isArray(object_code)) throw new Error("object_code is required.");
if (!Array.isArray(shop_items)) throw new Error("shop_items is required.");
@ -107,8 +107,8 @@ export class QuestModel {
this.objects = this._objects;
this._npcs = list_property(undefined, ...npcs);
this.npcs = this._npcs;
this._event_chains = list_property(undefined, ...event_chains);
this.event_chains = this._event_chains;
this._event_dags = list_property(undefined, ...event_dags);
this.event_dags = this._event_dags;
this.dat_unknowns = dat_unknowns;
this.object_code = object_code;
this.shop_items = shop_items;

View File

@ -6,8 +6,8 @@ import { Euler } from "three";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { QuestEventModel } from "../model/QuestEventModel";
import {
DatEvent,
DatEventAction,
DatEventActionTriggerEvent,
DatEventActionType,
} from "../../core/data_formats/parsing/quest/dat";
import {
@ -15,75 +15,13 @@ import {
QuestEventActionSpawnNpcsModel,
QuestEventActionUnlockModel,
} from "../model/QuestEventActionModel";
import { QuestEventChainModel } from "../model/QuestEventChainModel";
import { QuestEventDagModel, QuestEventDagModelMeta } from "../model/QuestEventDagModel";
import { QuestEvent } from "../../core/data_formats/parsing/quest/entities";
import Logger from "js-logger";
const logger = Logger.get("quest_editor/stores/model_conversion");
export function convert_quest_to_model(quest: Quest): QuestModel {
// Build up event chains.
const events = quest.events.slice();
const event_chains: QuestEventChainModel[] = [];
while (events.length) {
let event: QuestEvent | undefined = events.shift();
const chain_events = [];
while (event) {
chain_events.push(
new QuestEventModel(
event.id,
event.section_id,
event.wave,
event.delay,
event.actions
.filter(action => action.type !== DatEventActionType.TriggerEvent)
.map(action => {
switch (action.type) {
case DatEventActionType.SpawnNpcs:
return new QuestEventActionSpawnNpcsModel(
action.section_id,
action.appear_flag,
);
case DatEventActionType.Unlock:
return new QuestEventActionUnlockModel(action.door_id);
case DatEventActionType.Lock:
return new QuestEventActionLockModel(action.door_id);
case DatEventActionType.TriggerEvent:
throw new Error("Can't convert trigger event actions.");
}
}),
event.area_id,
event.unknown,
),
);
const event_id = event.id;
const trigger_event_actions = event.actions.filter(
action => action.type === DatEventActionType.TriggerEvent,
) as DatEventActionTriggerEvent[];
event = undefined;
if (trigger_event_actions.length >= 1) {
if (trigger_event_actions.length > 1) {
logger.warn(`Event ${event_id} has more than 1 trigger event action.`);
}
const index = events.findIndex(e => e.id === trigger_event_actions[0].event_id);
if (index !== -1) {
event = events.splice(index, 1)[0];
}
}
}
const chain = new QuestEventChainModel(chain_events);
event_chains.push(chain);
}
// Create quest model.
return new QuestModel(
quest.id,
@ -123,64 +61,189 @@ export function convert_quest_to_model(quest: Quest): QuestModel {
npc.unknown,
),
),
event_chains,
build_event_dags(quest.events),
quest.dat_unknowns,
quest.object_code,
quest.shop_items,
);
}
export function convert_quest_from_model(quest: QuestModel): Quest {
const events: QuestEvent[] = [];
function build_event_dags(dat_events: readonly DatEvent[]): QuestEventDagModel[] {
// Build up a temporary data structure with partial data.
// Maps id, section id and area id to data.
const data_map = new Map<
string,
{
event?: QuestEventModel;
parents: QuestEventModel[];
child_ids: number[];
}
>();
for (const chain of quest.event_chains.val) {
for (let i = 0; i < chain.events.length.val; i++) {
const event = chain.events.get(i);
const next_event: QuestEventModel | undefined = chain.events.get(i + 1);
for (const event of dat_events) {
const key = `${event.id}-${event.section_id}-${event.area_id}`;
let data = data_map.get(key);
const actions: DatEventAction[] = event.actions.map(action => {
if (action instanceof QuestEventActionSpawnNpcsModel) {
return {
type: DatEventActionType.SpawnNpcs,
section_id: action.section_id,
appear_flag: action.appear_flag,
};
} else if (action instanceof QuestEventActionUnlockModel) {
return {
type: DatEventActionType.Unlock,
door_id: action.door_id,
};
} else if (action instanceof QuestEventActionLockModel) {
return {
type: DatEventActionType.Lock,
door_id: action.door_id,
};
} else {
throw new Error(
`Unknown event action type ${Object.getPrototypeOf(action).constructor}`,
);
}
});
let event_model: QuestEventModel;
if (next_event) {
actions.push({
type: DatEventActionType.TriggerEvent,
event_id: next_event.id,
});
if (data && data.event) {
event_model = data.event;
} else {
event_model = new QuestEventModel(
event.id,
event.section_id,
event.wave,
event.delay,
event.area_id,
event.unknown,
);
if (data) {
data.event = event_model;
} else {
data = {
event: event_model,
parents: [],
child_ids: [],
};
data_map.set(key, data);
}
}
events.push({
id: event.id,
section_id: event.section_id,
wave: event.wave,
delay: event.delay,
actions,
area_id: event.area_id,
unknown: event.unknown,
});
for (const action of event.actions) {
switch (action.type) {
case DatEventActionType.SpawnNpcs:
event_model.add_action(
new QuestEventActionSpawnNpcsModel(action.section_id, action.appear_flag),
);
break;
case DatEventActionType.Unlock:
event_model.add_action(new QuestEventActionUnlockModel(action.door_id));
break;
case DatEventActionType.Lock:
event_model.add_action(new QuestEventActionLockModel(action.door_id));
break;
case DatEventActionType.TriggerEvent:
{
data.child_ids.push(action.event_id);
const child_key = `${action.event_id}-${event.section_id}-${event.area_id}`;
const child_data = data_map.get(child_key);
if (child_data) {
child_data.parents.push(event_model);
} else {
data_map.set(child_key, {
parents: [event_model],
child_ids: [],
});
}
}
break;
default:
logger.warn(`Unknown event action type: ${(action as any).type}.`);
break;
}
}
}
// Convert temporary structure to complete data structure used to build DAGs. Events that call
// nonexistent events are filtered out. This final structure is completely sound.
const event_to_full_data = new Map<QuestEventModel, QuestEventDagModelMeta>();
const root_events: QuestEventModel[] = [];
for (const data of data_map.values()) {
if (data.event) {
const children: QuestEventModel[] = [];
for (const child_id of data.child_ids) {
const child = data_map.get(
`${child_id}-${data.event.section_id}-${data.event.area_id}`,
)!;
if (child.event) {
children.push(child.event);
} else {
logger.warn(`Event ${data.event.id} calls nonexistent event ${child_id}.`);
}
}
event_to_full_data.set(data.event, {
parents: data.parents,
children,
});
if (data.parents.length === 0) {
root_events.push(data.event);
}
}
}
// Build DAGs from final complete data structure.
const event_dags: QuestEventDagModel[] = [];
while (root_events.length) {
const event = root_events.shift()!;
const dag_events: QuestEventModel[] = [];
const dag_root_events: QuestEventModel[] = [];
const dag_meta: Map<QuestEventModel, QuestEventDagModelMeta> = new Map();
// Start from a root event and find all connected events.
find_dag_events(
event_to_full_data,
dag_events,
dag_root_events,
dag_meta,
event,
new Set(),
);
for (const event of dag_root_events) {
const i = root_events.indexOf(event);
if (i !== -1) {
root_events.splice(i, 1);
}
}
event_dags.push(new QuestEventDagModel(dag_events, dag_root_events, dag_meta));
}
return event_dags;
}
function find_dag_events(
full_data: Map<QuestEventModel, QuestEventDagModelMeta>,
dag_events: QuestEventModel[],
dag_root_events: QuestEventModel[],
dag_meta: Map<QuestEventModel, QuestEventDagModelMeta>,
event: QuestEventModel,
visited: Set<QuestEventModel>,
): void {
if (visited.has(event)) return;
visited.add(event);
const data = full_data.get(event)!;
dag_events.push(event);
dag_meta.set(event, data);
if (data.parents.length === 0) {
dag_root_events.push(event);
}
for (const parent of data.parents) {
find_dag_events(full_data, dag_events, dag_root_events, dag_meta, parent, visited);
}
for (const child of data.children) {
find_dag_events(full_data, dag_events, dag_root_events, dag_meta, child, visited);
}
}
export function convert_quest_from_model(quest: QuestModel): Quest {
return {
id: quest.id.val,
language: quest.language.val,
@ -212,10 +275,61 @@ export function convert_quest_from_model(quest: QuestModel): Quest {
script_label: npc.script_label,
pso_roaming: npc.pso_roaming,
})),
events,
events: convert_quest_events_from_model(quest.event_dags.val),
dat_unknowns: quest.dat_unknowns,
object_code: quest.object_code,
shop_items: quest.shop_items,
map_designations: quest.map_designations.val,
};
}
function convert_quest_events_from_model(event_dags: readonly QuestEventDagModel[]): QuestEvent[] {
const events: QuestEvent[] = [];
for (const event_dag of event_dags) {
for (const event of event_dag.events) {
const actions: DatEventAction[] = event.actions.val.map(action => {
if (action instanceof QuestEventActionSpawnNpcsModel) {
return {
type: DatEventActionType.SpawnNpcs,
section_id: action.section_id,
appear_flag: action.appear_flag,
};
} else if (action instanceof QuestEventActionUnlockModel) {
return {
type: DatEventActionType.Unlock,
door_id: action.door_id,
};
} else if (action instanceof QuestEventActionLockModel) {
return {
type: DatEventActionType.Lock,
door_id: action.door_id,
};
} else {
throw new Error(
`Unknown event action type ${Object.getPrototypeOf(action).constructor}`,
);
}
});
for (const child_event of event_dag.get_children(event)) {
actions.push({
type: DatEventActionType.TriggerEvent,
event_id: child_event.id,
});
}
events.push({
id: event.id,
section_id: event.section_id,
wave: event.wave,
delay: event.delay,
actions,
area_id: event.area_id,
unknown: event.unknown,
});
}
}
return events;
}

View File

@ -17,7 +17,7 @@ import {
import { QuestObjectModel } from "../model/QuestObjectModel";
import { QuestNpcModel } from "../model/QuestNpcModel";
import { Euler, Vector3 } from "three";
import { QuestEventChainModel } from "../model/QuestEventChainModel";
import { QuestEventDagModel } from "../model/QuestEventDagModel";
export function create_new_quest(episode: Episode): QuestModel {
if (episode === Episode.II) throw new Error("Episode II not yet supported.");
@ -807,6 +807,6 @@ function create_default_npcs(): QuestNpcModel[] {
];
}
function create_default_event_chains(): QuestEventChainModel[] {
function create_default_event_chains(): QuestEventDagModel[] {
return [];
}