diff --git a/packages/core-editor/src/common/context/event-emitter.type.ts b/packages/core-editor/src/common/context/event-emitter.type.ts index 790bddd1..15cb504a 100644 --- a/packages/core-editor/src/common/context/event-emitter.type.ts +++ b/packages/core-editor/src/common/context/event-emitter.type.ts @@ -1,15 +1,15 @@ -/** - * Events that the NanoForge editor can emit to the running engine. - */ -export enum EventTypeEnum { - /** Reload modules without restarting the engine (live-patch). */ - HOT_RELOAD = "hot-reload", - /** Fully restart the engine with the latest changes. */ - HARD_RELOAD = "hard-reload", -} - /** Callback signature for event listeners. */ -export type ListenerType = (...args: any[]) => void; +export type ListenerType< + Events extends string, + EventsMap extends Record, + K extends keyof EventsMap, +> = (...args: EventsMap[K]) => void; + +/** Signature of the waiting for execution events. */ +export type QueuedEvent = { + event: K; + args: EventsMap[K]; +}; /** * Simple event emitter interface used for communication between the NanoForge @@ -19,14 +19,17 @@ export type ListenerType = (...args: any[]) => void; * Listeners are queued and processed via `runEvents` to keep the engine * loop deterministic. */ -export interface IEventEmitter { +export interface IEventEmitter> { /** Map of event names to their registered listeners. */ - listeners: Record; + listeners: { + [K in keyof EventsMap]?: ListenerType[]; + }; + /** Queue of events waiting to be dispatched by `runEvents`. */ - eventQueue: { event: EventTypeEnum | string; args: any[] }[]; + eventQueue: QueuedEvent[]; /** Drain the event queue and invoke all matching listeners. */ - runEvents: () => void; + runEvents(): void; /** * Enqueue an event for dispatching on the next `runEvents` call. @@ -34,7 +37,7 @@ export interface IEventEmitter { * @param event - Event name or EventTypeEnum value. * @param args - Optional arguments forwarded to listeners. */ - emitEvent: (event: EventTypeEnum, ...args: any) => void; + emitEvent(event: K, ...args: EventsMap[K]): void; /** * Register a listener for an event. Alias: `on`. @@ -42,9 +45,12 @@ export interface IEventEmitter { * @param event - Event name to subscribe to. * @param listener - Callback invoked when the event fires. */ - addListener: (event: EventTypeEnum | string, listener: ListenerType) => void; + addListener( + event: K, + listener: ListenerType, + ): void; /** Alias for `addListener`. */ - on: (event: EventTypeEnum | string, listener: ListenerType) => void; + on(event: K, listener: ListenerType): void; /** * Remove a previously registered listener. Alias: `off`. @@ -52,16 +58,19 @@ export interface IEventEmitter { * @param event - Event name to unsubscribe from. * @param listener - The exact listener function to remove. */ - removeListener: (event: EventTypeEnum | string, listener: ListenerType) => void; + removeListener( + event: K, + listener: ListenerType, + ): void; /** Alias for `removeListener`. */ - off: (event: EventTypeEnum | string, listener: ListenerType) => void; + off(event: K, listener: ListenerType): void; /** * Remove all listeners registered for a specific event. * * @param event - Event name whose listeners should be cleared. */ - removeListenersForEvent: (event: EventTypeEnum | string) => void; + removeListenersForEvent(event: keyof EventsMap): void; /** Remove every registered listener across all events. */ - removeAllListeners: () => void; + removeAllListeners(): void; } diff --git a/packages/core-editor/src/common/context/events/core-events.ts b/packages/core-editor/src/common/context/events/core-events.ts new file mode 100644 index 00000000..03d041f0 --- /dev/null +++ b/packages/core-editor/src/common/context/events/core-events.ts @@ -0,0 +1,25 @@ +import { type Save } from "../save.type"; + +/** + * Events that the NanoForge editor can emit to the running engine. + */ +export enum CoreEvents { + /** Reload only changed entity components params (live-patch) */ + HOT_RELOAD = "hot-reload", + /** Reload all entities component params */ + HARD_RELOAD = "hard-reload", + /** Don't execute the run function until UNPAUSE_GAME */ + PAUSE_GAME = "pause-game", + /** End main loop and clear */ + STOP_GAME = "stop-game", + /** resume executing the run function */ + UNPAUSE_GAME = "unpause-game", +} + +export interface CoreEventsMap { + [CoreEvents.HOT_RELOAD]: [save: Save]; + [CoreEvents.HARD_RELOAD]: [save: Save]; + [CoreEvents.PAUSE_GAME]: []; + [CoreEvents.STOP_GAME]: []; + [CoreEvents.UNPAUSE_GAME]: []; +} diff --git a/packages/core-editor/src/common/context/events/editor-events.ts b/packages/core-editor/src/common/context/events/editor-events.ts new file mode 100644 index 00000000..f340b43c --- /dev/null +++ b/packages/core-editor/src/common/context/events/editor-events.ts @@ -0,0 +1,9 @@ +// ! Please do not remove this event unless a new one replaces it, it causes types issues + +export enum EditorEvents { + EMPTY = "empty", +} + +export interface EditorEventsMap { + [EditorEvents.EMPTY]: []; +} diff --git a/packages/core-editor/src/common/context/options.type.ts b/packages/core-editor/src/common/context/options.type.ts index d8a09fdf..c2bd724f 100644 --- a/packages/core-editor/src/common/context/options.type.ts +++ b/packages/core-editor/src/common/context/options.type.ts @@ -1,4 +1,6 @@ import { type IEventEmitter } from "./event-emitter.type"; +import { type CoreEvents, type CoreEventsMap } from "./events/core-events"; +import { type EditorEvents, type EditorEventsMap } from "./events/editor-events"; import { type Save } from "./save.type"; /** @@ -25,9 +27,9 @@ export interface IEditorRunClientOptions { /** Serialised scene state loaded or saved by the editor. */ save: Save; /** Event emitter for core-to-editor communication. */ - coreEvents: IEventEmitter; + coreEvents: IEventEmitter; /** Event emitter for editor-to-core communication. */ - editorEvents: IEventEmitter; + editorEvents: IEventEmitter; }; } @@ -47,8 +49,8 @@ export interface IEditorRunServerOptions { /** Serialised scene state loaded or saved by the editor. */ save: Save; /** Event emitter for core-to-editor communication. */ - coreEvents: IEventEmitter; + coreEvents: IEventEmitter; /** Event emitter for editor-to-core communication. */ - editorEvents: IEventEmitter; + editorEvents: IEventEmitter; }; } diff --git a/packages/core-editor/src/common/context/save.type.ts b/packages/core-editor/src/common/context/save.type.ts index 2337b8bc..d29f2987 100644 --- a/packages/core-editor/src/common/context/save.type.ts +++ b/packages/core-editor/src/common/context/save.type.ts @@ -57,7 +57,7 @@ export interface SaveEntity { } /** - * Root serialised scene state exchanged between the editor and the engine. + * Root serialized scene state exchanged between the editor and the engine. */ export interface Save { /** All libraries registered in the scene. */ diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts index b917ce2e..4676e744 100644 --- a/packages/core-editor/src/core/core.ts +++ b/packages/core-editor/src/core/core.ts @@ -36,6 +36,7 @@ export class Core { this._configRegistry = new ConfigRegistry(options.env); await this.runInit(this.getInitContext(options)); this.editor = new CoreEditor( + this, options.editor, this.config.getComponentSystemLibrary().library, ); @@ -63,7 +64,7 @@ export class Core { return; } const tickStart = Date.now(); - await runner(tickStart - previousTick); + if (this.editor?.isPaused) await runner(tickStart - previousTick); previousTick = tickStart; setTimeout(render, tickLengthMs + tickStart - Date.now()); }; @@ -72,16 +73,16 @@ export class Core { setTimeout(render); } + public getExecutionContext(): EditableExecutionContext { + return new EditableExecutionContext(this.context, this.config.libraryManager); + } + private getInitContext(options: IEditorRunOptions): InitContext { if (!this._configRegistry) throw new NfNotInitializedException("Core"); return new InitContext(this.context, this.config.libraryManager, this._configRegistry, options); } - private getExecutionContext(): EditableExecutionContext { - return new EditableExecutionContext(this.context, this.config.libraryManager); - } - private getClearContext(): ClearContext { return new ClearContext(this.context, this.config.libraryManager); } diff --git a/packages/core-editor/src/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts index f9bb7494..40e52ade 100644 --- a/packages/core-editor/src/editor/core-editor.ts +++ b/packages/core-editor/src/editor/core-editor.ts @@ -1,33 +1,68 @@ import { NfNotFound } from "@nanoforge-dev/common"; import { type ECSClientLibrary, type Entity } from "@nanoforge-dev/ecs-client"; -import { EventTypeEnum } from "../common/context/event-emitter.type"; +import { CoreEvents } from "../common/context/events/core-events"; import { type IEditorRunOptions } from "../common/context/options.type"; +import { type Save } from "../common/context/save.type"; +import { type Core } from "../core/core"; export class CoreEditor { private editor: IEditorRunOptions["editor"]; private ecsLibrary: ECSClientLibrary; - constructor(editor: IEditorRunOptions["editor"], ecsLibrary: ECSClientLibrary) { + private lastLoadedSave: Save; + private core: Core; + private _isPaused: boolean = false; + + constructor(core: Core, editor: IEditorRunOptions["editor"], ecsLibrary: ECSClientLibrary) { this.editor = editor; + this.lastLoadedSave = JSON.parse(JSON.stringify(this.editor.save)); this.ecsLibrary = ecsLibrary; - this.editor.coreEvents?.addListener( - EventTypeEnum.HOT_RELOAD, - this.askEntitiesHotReload.bind(this), - ); + this.editor.coreEvents?.addListener(CoreEvents.HOT_RELOAD, this.hotReloadEvent.bind(this)); + this.editor.coreEvents?.addListener(CoreEvents.HARD_RELOAD, this.hardReloadEvent.bind(this)); + this.editor.coreEvents?.addListener(CoreEvents.PAUSE_GAME, this.pauseGameEvent.bind(this)); + this.editor.coreEvents?.addListener(CoreEvents.STOP_GAME, this.stopGameEvent.bind(this)); + this.editor.coreEvents?.addListener(CoreEvents.UNPAUSE_GAME, this.unpauseGameEvent.bind(this)); + this.core = core; + } + + get isPaused(): boolean { + return this._isPaused; } public runEvents() { this.editor.coreEvents?.runEvents(); } - public askEntitiesHotReload(): void { + public hotReloadEvent(save: Save): void { const reg = this.ecsLibrary.registry; - const save = this.editor.save; + this.lastLoadedSave = JSON.parse(JSON.stringify(save)); save.entities.forEach(({ id, components }) => { Object.entries(components).forEach(([componentName, params]) => { - const ogComponent = save.components.find(({ name: paramName }) => { - return paramName == componentName; + 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); + }); + }); + } + + public hardReloadEvent(save: Save): 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); if (!ogComponent) { throw new NfNotFound("Component: " + componentName + " not found in saved components"); } @@ -43,6 +78,17 @@ export class CoreEditor { }); } + public pauseGameEvent(): void { + this._isPaused = true; + } + public unpauseGameEvent(): void { + this._isPaused = false; + } + + public stopGameEvent(): void { + this.core.getExecutionContext().application.setIsRunning(false); + } + private getEntityFromEntityId(entityId: string): Entity { const reg = this.ecsLibrary.registry; return reg.entityFromIndex( diff --git a/packages/core-editor/test/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts index ead57efb..9dfc5a93 100644 --- a/packages/core-editor/test/editor-feature.spec.ts +++ b/packages/core-editor/test/editor-feature.spec.ts @@ -1,9 +1,10 @@ import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { EventTypeEnum } from "../src/common/context/event-emitter.type"; +import { CoreEvents } from "../src/common/context/events/core-events"; import type { IEditorRunOptions } from "../src/common/context/options.type"; import { type Save, type SaveComponent, type SaveEntity } from "../src/common/context/save.type"; +import { type Core } from "../src/core/core"; import { CoreEditor } from "../src/editor/core-editor"; import { EventEmitter } from "./helpers/event-emitter"; @@ -15,13 +16,14 @@ describe("EditorFeatures", () => { describe("eventEmitter", () => { it("should execute eventQueue once", async () => { const events = new EventEmitter(); - events.emitEvent(EventTypeEnum.HOT_RELOAD); - events.emitEvent(EventTypeEnum.HOT_RELOAD); + events.emitEvent(CoreEvents.HOT_RELOAD); + events.emitEvent(CoreEvents.HOT_RELOAD); const spyHotReload = vi - .spyOn(CoreEditor.prototype, "askEntitiesHotReload") + .spyOn(CoreEditor.prototype, "hotReloadEvent") .mockImplementation(() => {}); new CoreEditor( - { coreEvents: events } as IEditorRunOptions["editor"], + {} as unknown as Core, + { coreEvents: events, save: { libraries: [] } } as unknown as IEditorRunOptions["editor"], {} as ECSClientLibrary, ).runEvents(); expect(spyHotReload).toHaveBeenCalledTimes(2); @@ -113,6 +115,7 @@ describe("EditorFeatures", () => { ]; const fakeReg = new FakeRegistry(); new CoreEditor( + {} as unknown as Core, { save: { components, @@ -120,7 +123,7 @@ describe("EditorFeatures", () => { } as any as Save, } as any as IEditorRunOptions["editor"], { registry: fakeReg } as any as ECSClientLibrary, - ).askEntitiesHotReload(); + ).hotReloadEvent({ components, entities } as any as Save); expect(fakeReg.getComponents).toHaveBeenCalledWith({ name: "__RESERVED_ENTITY_ID" }); expect(getIndex).toHaveBeenNthCalledWith(1, { entityId: "ent2", @@ -137,13 +140,13 @@ describe("EditorFeatures", () => { expect(fakeReg.getEntityComponent).toHaveBeenNthCalledWith(1, 2, { name: "Position" }); expect(fakeReg.getEntityComponent).toHaveBeenNthCalledWith(2, 2, { name: "Bullets" }); expect(fakeReg.getEntityComponent).toHaveBeenNthCalledWith(3, 3, { name: "Position" }); - expect(fakeReg.addComponent).toHaveBeenNthCalledWith(1, 2, { name: "Position", x: 1, y: 2 }); + expect(fakeReg.addComponent).toHaveBeenNthCalledWith(1, 2, { name: "Position", x: 3, y: 4 }); expect(fakeReg.addComponent).toHaveBeenNthCalledWith(2, 2, { name: "Bullets", bulletTypes: ["fire", "water", "rocket"], - number: 1000, + number: 4, }); - expect(fakeReg.addComponent).toHaveBeenNthCalledWith(3, 3, { name: "Position", x: 5, y: 6 }); + expect(fakeReg.addComponent).toHaveBeenNthCalledWith(3, 3, { name: "Position", x: 7, y: 8 }); }); }); }); diff --git a/packages/ecs-lib/lib/libecs.d.ts b/packages/ecs-lib/lib/libecs.d.ts index 6ff945f1..d4d8cf0b 100644 --- a/packages/ecs-lib/lib/libecs.d.ts +++ b/packages/ecs-lib/lib/libecs.d.ts @@ -16,7 +16,7 @@ export interface ClassHandle { [Symbol.dispose](): void; clone(): this; } -export interface container extends ClassHandle { +export interface container extends ClassHandle, Iterable { size(): number; get(_0: number): any | undefined | undefined; push_back(_0?: any): void; diff --git a/packages/ecs-lib/wasm/Registry.hpp b/packages/ecs-lib/wasm/Registry.hpp index eda9d55f..4cde1ee0 100644 --- a/packages/ecs-lib/wasm/Registry.hpp +++ b/packages/ecs-lib/wasm/Registry.hpp @@ -45,7 +45,7 @@ namespace nfo { SparseArray ®ister_component(const Component &component) { std::string component_type(get_js_class_name(component)); - if (component_type == "entity" || component_type == "id") + if (component_type == "entity" || component_type == "id" || component_type == UNKNOWN_COMPONENT_TYPE) throw std::runtime_error("Component type '" + component_type + "' not supported : you can't use : id, entity, " + UNKNOWN_COMPONENT_TYPE); if (!_components_arrays.contains(component_type)) _components_arrays.emplace(component_type, SparseArray());