Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions packages/core-editor/src/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
NfNotInitializedException,
} from "@nanoforge-dev/common";
import { type IRunOptions } from "@nanoforge-dev/common";
import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client";

import { type ApplicationConfig } from "../../../core/src/application/application-config";
import type { IApplicationOptions } from "../../../core/src/application/application-options.type";
Expand All @@ -35,11 +34,7 @@ export class Core {
public async init(options: IRunOptions, appOptions: IApplicationOptions): Promise<void> {
this.options = appOptions;
this._configRegistry = new ConfigRegistry(options.env);
this.editor = new CoreEditor(
this,
options.editor,
this.config.getComponentSystemLibrary<ECSClientLibrary>().library,
);
this.editor = new CoreEditor(this, options.editor, this.config);
await this.runInit(this.getInitContext(options));
}

Expand Down
77 changes: 40 additions & 37 deletions packages/core-editor/src/editor/core-editor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { type IRunOptions, NfNotFound } from "@nanoforge-dev/common";
import { type IAssetManagerLibrary, type IRunOptions, NfNotFound } from "@nanoforge-dev/common";
import type { ECSClientLibrary, Entity } from "@nanoforge-dev/ecs-client";

import type { ApplicationConfig } from "../../../core/src/application/application-config";
import { EventEmitter } from "../common/context/event-emitter";
import { CoreEvents } from "../common/context/events/core-events";
import type { Save } from "../common/context/save.type";
Expand All @@ -9,19 +10,24 @@ import type { Core } from "../core/core";
export class CoreEditor {
public eventEmitter: EventEmitter;
private ecsLibrary: ECSClientLibrary;
private assetLibrary: IAssetManagerLibrary;
private lastLoadedSave: Save;
private core: Core;
private _isPaused: boolean = false;

constructor(core: Core, editor: IRunOptions["editor"], ecsLibrary: ECSClientLibrary) {
constructor(core: Core, editor: IRunOptions["editor"], config: ApplicationConfig) {
this.eventEmitter = new EventEmitter(editor);
this.lastLoadedSave = JSON.parse(JSON.stringify(editor.save));
this.ecsLibrary = ecsLibrary;

this.ecsLibrary = config.getComponentSystemLibrary<ECSClientLibrary>().library;
this.assetLibrary = config.getAssetManagerLibrary().library;

this.eventEmitter.on(CoreEvents.HOT_RELOAD, this.hotReloadEvent.bind(this));
this.eventEmitter.on(CoreEvents.HARD_RELOAD, this.hardReloadEvent.bind(this));
this.eventEmitter.on(CoreEvents.PAUSE_GAME, this.pauseGameEvent.bind(this));
this.eventEmitter.on(CoreEvents.STOP_GAME, this.stopGameEvent.bind(this));
this.eventEmitter.on(CoreEvents.UNPAUSE_GAME, this.unpauseGameEvent.bind(this));

this.core = core;
}

Expand All @@ -34,32 +40,26 @@ export class CoreEditor {
}

public hotReloadEvent(save: Save): void {
const reg = this.ecsLibrary.registry;
save.entities.forEach(({ id, components }) => {
Object.entries(components).forEach(([componentName, params]) => {
const ogComponent = save.components.find(({ name }) => name === componentName);
if (!ogComponent) {
throw new NfNotFound("Component: " + componentName + " not found in saved components");
}
const ecsEntity: Entity = this.getEntityFromEntityId(id);
const ecsComponent = reg.getEntityComponent(ecsEntity, {
name: componentName,
});
Object.entries(params).forEach(([paramName, paramValue]) => {
const lastLoadedParam = this.lastLoadedSave.entities.find((e) => e.id === id)?.components[
componentName
]?.[paramName];
if (lastLoadedParam !== paramValue) ecsComponent[paramName] = paramValue;
});
reg.addComponent(ecsEntity, ecsComponent);
});
});
this.lastLoadedSave = JSON.parse(JSON.stringify(save));
this.reloadEvent(save, false);
}

public hardReloadEvent(save: Save): void {
this.reloadEvent(save, true);
}

public pauseGameEvent(): void {
this._isPaused = true;
}
public unpauseGameEvent(): void {
this._isPaused = false;
}

public stopGameEvent(): void {
this.core.getExecutionContext().application.setIsRunning(false);
}

private reloadEvent(save: Save, hard: boolean): void {
const reg = this.ecsLibrary.registry;
this.lastLoadedSave = JSON.parse(JSON.stringify(save));
save.entities.forEach(({ id, components }) => {
Object.entries(components).forEach(([componentName, params]) => {
const ogComponent = save.components.find(({ name }) => name === componentName);
Expand All @@ -71,22 +71,25 @@ export class CoreEditor {
name: componentName,
});
Object.entries(params).forEach(([paramName, paramValue]) => {
ecsComponent[paramName] = paramValue;
if (!hard) {
const lastLoadedParam = this.lastLoadedSave.entities.find((e) => e.id === id)
?.components[componentName]?.[paramName];
if (lastLoadedParam === paramValue) return;
}

const ogParam = ogComponent.paramsNames.find(
(param) => param === paramName || param === `__RESERVED_ASSET_${paramName}`,
);
if (!ogParam) return;

ecsComponent[paramName] = ogParam.startsWith("__RESERVED_ASSET_")
? this.assetLibrary.getAsset(paramValue)
: paramValue;
});
reg.addComponent(ecsEntity, ecsComponent);
});
});
}

public pauseGameEvent(): void {
this._isPaused = true;
}
public unpauseGameEvent(): void {
this._isPaused = false;
}

public stopGameEvent(): void {
this.core.getExecutionContext().application.setIsRunning(false);
this.lastLoadedSave = JSON.parse(JSON.stringify(save));
}

private getEntityFromEntityId(entityId: string): Entity {
Expand Down
14 changes: 11 additions & 3 deletions packages/core-editor/test/editor-feature.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type IRunOptions } from "@nanoforge-dev/common";
import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client";
import { afterEach, describe, expect, it, vi } from "vitest";

import { type ApplicationConfig } from "../../core/src/application/application-config";
import { CoreEvents } from "../src/common/context/events/core-events";
import { type Save, type SaveComponent, type SaveEntity } from "../src/common/context/save.type";
import { type Core } from "../src/core/core";
Expand All @@ -24,7 +24,10 @@ describe("EditorFeatures", () => {
new CoreEditor(
{} as unknown as Core,
{ coreEvents: events, save: { libraries: [] } } as unknown as IRunOptions["editor"],
{} as ECSClientLibrary,
{
getAssetManagerLibrary: () => ({ library: {} }),
getComponentSystemLibrary: () => ({ library: {} }),
} as ApplicationConfig,
).runEvents();
expect(spyHotReload).toHaveBeenCalledTimes(2);
});
Expand Down Expand Up @@ -126,7 +129,12 @@ describe("EditorFeatures", () => {
} as any as Save,
coreEvents: events,
} as any as IRunOptions["editor"],
{ registry: fakeReg } as any as ECSClientLibrary,
{
getAssetManagerLibrary: () => ({
library: { getAsset: vi.fn((name: string) => ({ path: name })) },
}),
getComponentSystemLibrary: () => ({ library: { registry: fakeReg } }),
} as unknown as ApplicationConfig,
).hotReloadEvent({ components, entities } as any as Save);
expect(fakeReg.getComponents).toHaveBeenCalledWith({ name: "__RESERVED_entityId" });
expect(getIndex).toHaveBeenNthCalledWith(1, {
Expand Down
70 changes: 51 additions & 19 deletions packages/core-editor/test/helpers/event-emitter.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,82 @@
import {
type EventTypeEnum,
type IEventEmitter,
type ListenerType,
type QueuedEvent,
} from "../../src/common/context/event-emitter.type";

export class EventEmitter implements IEventEmitter {
public listeners: Record<EventTypeEnum | string, ListenerType[]> = {};
public eventQueue: { event: EventTypeEnum | string; args: any[] }[] = [];
export class EventEmitter<
Events extends string,
EventsMap extends Record<Events, unknown[]>,
> implements IEventEmitter<Events, EventsMap> {
public listeners: {
[K in keyof EventsMap]?: ListenerType<Events, EventsMap, K>[];
} = {};

public runEvents = () => {
this.eventQueue.forEach(({ event, args }) => {
this.listeners[event]?.forEach((listener) => {
listener(...args);
});
public eventQueue: QueuedEvent<EventsMap>[] = [];
private readonly _dequeueOnEmit: boolean;

constructor(dequeueOnEmit = false) {
this._dequeueOnEmit = dequeueOnEmit;
}

runEvents(): void {
this.eventQueue.forEach((e) => {
this._executeEvent(e);
});
this.eventQueue = [];
};

public emitEvent(event: EventTypeEnum | string, ...args: any[]) {
this.eventQueue.push({ event, args });
this.eventQueue = [];
}

public addListener(event: EventTypeEnum | string, listener: ListenerType): void {
emitEvent<K extends keyof EventsMap>(event: K, ...args: EventsMap[K]): void {
this.eventQueue.push({
event,
args,
});
if (this._dequeueOnEmit) this.runEvents();
}
addListener<K extends keyof EventsMap>(
event: K,
listener: ListenerType<Events, EventsMap, K>,
): void {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event].push(listener);
}
public on(event: EventTypeEnum | string, listener: ListenerType): void {
on<K extends keyof EventsMap>(event: K, listener: ListenerType<Events, EventsMap, K>): void {
this.addListener(event, listener);
}

public removeListener(event: EventTypeEnum | string, listener: ListenerType): void {
removeListener<K extends keyof EventsMap>(
event: K,
listener: ListenerType<Events, EventsMap, K>,
): void {
if (!this.listeners[event]) return;
const index = this.listeners[event].indexOf(listener);
if (index >= 0) {
this.listeners[event].splice(index, 1);
}
}
public off(event: EventTypeEnum | string, listener: ListenerType): void {
off<K extends keyof EventsMap>(event: K, listener: ListenerType<Events, EventsMap, K>): void {
this.removeListener(event, listener);
}

public removeListenersForEvent(event: EventTypeEnum | string): void {
removeListenersForEvent(event: keyof EventsMap): void {
if (!this.listeners[event]) return;
this.listeners[event] = [];
}
public removeAllListeners(): void {
removeAllListeners(): void {
this.listeners = {};
}

private _executeEvent<K extends keyof EventsMap>({
event,
args,
}: QueuedEvent<EventsMap, K>): void {
this.listeners[event]?.forEach((listener) => {
try {
listener(...args);
} catch (error) {
console.error(`Error handling event [${String(event)}]:`, error);
}
});
}
}
13 changes: 12 additions & 1 deletion packages/ecs-lib/src/editor-manifest.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ type ECSNumberElement = ECSElementDefaults<"number", number>;
*/
type ECSBooleanElement = ECSElementDefaults<"boolean", boolean>;

/**
* * Editor Component Asset Element
* Type for asset element
*/
type ECSAssetElement = ECSElementDefaults<"asset", string>;

/**
* * Editor Component Array Element
* Type for array element
Expand Down Expand Up @@ -88,7 +94,12 @@ type ECSObjectElement = {
* Type for component element
*/
type ECSElement =
ECSStringElement | ECSNumberElement | ECSBooleanElement | ECSArrayElement | ECSObjectElement;
| ECSStringElement
| ECSNumberElement
| ECSBooleanElement
| ECSAssetElement
| ECSArrayElement
| ECSObjectElement;

/**
* Manifest for a component to be used in the NanoForge Editor
Expand Down
Loading