From 5dafca08b99f49f76a072f4a22e8f3c27126379d Mon Sep 17 00:00:00 2001 From: Exelo Date: Tue, 30 Jun 2026 22:36:18 +0900 Subject: [PATCH] feat: add assets handling --- packages/core-editor/src/core/core.ts | 7 +- .../core-editor/src/editor/core-editor.ts | 77 ++++++++++--------- .../core-editor/test/editor-feature.spec.ts | 14 +++- .../core-editor/test/helpers/event-emitter.ts | 70 ++++++++++++----- packages/ecs-lib/src/editor-manifest.type.ts | 13 +++- 5 files changed, 115 insertions(+), 66 deletions(-) diff --git a/packages/core-editor/src/core/core.ts b/packages/core-editor/src/core/core.ts index f2f8057..cbaddad 100644 --- a/packages/core-editor/src/core/core.ts +++ b/packages/core-editor/src/core/core.ts @@ -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"; @@ -35,11 +34,7 @@ export class Core { public async init(options: IRunOptions, appOptions: IApplicationOptions): Promise { this.options = appOptions; this._configRegistry = new ConfigRegistry(options.env); - this.editor = new CoreEditor( - this, - options.editor, - this.config.getComponentSystemLibrary().library, - ); + this.editor = new CoreEditor(this, options.editor, this.config); await this.runInit(this.getInitContext(options)); } diff --git a/packages/core-editor/src/editor/core-editor.ts b/packages/core-editor/src/editor/core-editor.ts index a968e89..0e1fe94 100644 --- a/packages/core-editor/src/editor/core-editor.ts +++ b/packages/core-editor/src/editor/core-editor.ts @@ -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"; @@ -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().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; } @@ -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); @@ -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 { diff --git a/packages/core-editor/test/editor-feature.spec.ts b/packages/core-editor/test/editor-feature.spec.ts index b12d0de..191cf94 100644 --- a/packages/core-editor/test/editor-feature.spec.ts +++ b/packages/core-editor/test/editor-feature.spec.ts @@ -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"; @@ -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); }); @@ -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, { diff --git a/packages/core-editor/test/helpers/event-emitter.ts b/packages/core-editor/test/helpers/event-emitter.ts index 95c6eb6..a06695f 100644 --- a/packages/core-editor/test/helpers/event-emitter.ts +++ b/packages/core-editor/test/helpers/event-emitter.ts @@ -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 = {}; - public eventQueue: { event: EventTypeEnum | string; args: any[] }[] = []; +export class EventEmitter< + Events extends string, + EventsMap extends Record, +> implements IEventEmitter { + public listeners: { + [K in keyof EventsMap]?: ListenerType[]; + } = {}; - public runEvents = () => { - this.eventQueue.forEach(({ event, args }) => { - this.listeners[event]?.forEach((listener) => { - listener(...args); - }); + public eventQueue: QueuedEvent[] = []; + 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(event: K, ...args: EventsMap[K]): void { + this.eventQueue.push({ + event, + args, + }); + if (this._dequeueOnEmit) this.runEvents(); + } + addListener( + event: K, + listener: ListenerType, + ): void { if (!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(listener); } - public on(event: EventTypeEnum | string, listener: ListenerType): void { + on(event: K, listener: ListenerType): void { this.addListener(event, listener); } - public removeListener(event: EventTypeEnum | string, listener: ListenerType): void { + removeListener( + event: K, + listener: ListenerType, + ): 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(event: K, listener: ListenerType): 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({ + event, + args, + }: QueuedEvent): void { + this.listeners[event]?.forEach((listener) => { + try { + listener(...args); + } catch (error) { + console.error(`Error handling event [${String(event)}]:`, error); + } + }); + } } diff --git a/packages/ecs-lib/src/editor-manifest.type.ts b/packages/ecs-lib/src/editor-manifest.type.ts index bf1af87..337706a 100644 --- a/packages/ecs-lib/src/editor-manifest.type.ts +++ b/packages/ecs-lib/src/editor-manifest.type.ts @@ -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 @@ -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