Skip to content

Commit cda797a

Browse files
authored
feat: add assets handling (#383)
1 parent afffa7a commit cda797a

5 files changed

Lines changed: 115 additions & 66 deletions

File tree

packages/core-editor/src/core/core.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
NfNotInitializedException,
1010
} from "@nanoforge-dev/common";
1111
import { type IRunOptions } from "@nanoforge-dev/common";
12-
import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client";
1312

1413
import { type ApplicationConfig } from "../../../core/src/application/application-config";
1514
import type { IApplicationOptions } from "../../../core/src/application/application-options.type";
@@ -35,11 +34,7 @@ export class Core {
3534
public async init(options: IRunOptions, appOptions: IApplicationOptions): Promise<void> {
3635
this.options = appOptions;
3736
this._configRegistry = new ConfigRegistry(options.env);
38-
this.editor = new CoreEditor(
39-
this,
40-
options.editor,
41-
this.config.getComponentSystemLibrary<ECSClientLibrary>().library,
42-
);
37+
this.editor = new CoreEditor(this, options.editor, this.config);
4338
await this.runInit(this.getInitContext(options));
4439
}
4540

packages/core-editor/src/editor/core-editor.ts

Lines changed: 40 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { type IRunOptions, NfNotFound } from "@nanoforge-dev/common";
1+
import { type IAssetManagerLibrary, type IRunOptions, NfNotFound } from "@nanoforge-dev/common";
22
import type { ECSClientLibrary, Entity } from "@nanoforge-dev/ecs-client";
33

4+
import type { ApplicationConfig } from "../../../core/src/application/application-config";
45
import { EventEmitter } from "../common/context/event-emitter";
56
import { CoreEvents } from "../common/context/events/core-events";
67
import type { Save } from "../common/context/save.type";
@@ -9,19 +10,24 @@ import type { Core } from "../core/core";
910
export class CoreEditor {
1011
public eventEmitter: EventEmitter;
1112
private ecsLibrary: ECSClientLibrary;
13+
private assetLibrary: IAssetManagerLibrary;
1214
private lastLoadedSave: Save;
1315
private core: Core;
1416
private _isPaused: boolean = false;
1517

16-
constructor(core: Core, editor: IRunOptions["editor"], ecsLibrary: ECSClientLibrary) {
18+
constructor(core: Core, editor: IRunOptions["editor"], config: ApplicationConfig) {
1719
this.eventEmitter = new EventEmitter(editor);
1820
this.lastLoadedSave = JSON.parse(JSON.stringify(editor.save));
19-
this.ecsLibrary = ecsLibrary;
21+
22+
this.ecsLibrary = config.getComponentSystemLibrary<ECSClientLibrary>().library;
23+
this.assetLibrary = config.getAssetManagerLibrary().library;
24+
2025
this.eventEmitter.on(CoreEvents.HOT_RELOAD, this.hotReloadEvent.bind(this));
2126
this.eventEmitter.on(CoreEvents.HARD_RELOAD, this.hardReloadEvent.bind(this));
2227
this.eventEmitter.on(CoreEvents.PAUSE_GAME, this.pauseGameEvent.bind(this));
2328
this.eventEmitter.on(CoreEvents.STOP_GAME, this.stopGameEvent.bind(this));
2429
this.eventEmitter.on(CoreEvents.UNPAUSE_GAME, this.unpauseGameEvent.bind(this));
30+
2531
this.core = core;
2632
}
2733

@@ -34,32 +40,26 @@ export class CoreEditor {
3440
}
3541

3642
public hotReloadEvent(save: Save): void {
37-
const reg = this.ecsLibrary.registry;
38-
save.entities.forEach(({ id, components }) => {
39-
Object.entries(components).forEach(([componentName, params]) => {
40-
const ogComponent = save.components.find(({ name }) => name === componentName);
41-
if (!ogComponent) {
42-
throw new NfNotFound("Component: " + componentName + " not found in saved components");
43-
}
44-
const ecsEntity: Entity = this.getEntityFromEntityId(id);
45-
const ecsComponent = reg.getEntityComponent(ecsEntity, {
46-
name: componentName,
47-
});
48-
Object.entries(params).forEach(([paramName, paramValue]) => {
49-
const lastLoadedParam = this.lastLoadedSave.entities.find((e) => e.id === id)?.components[
50-
componentName
51-
]?.[paramName];
52-
if (lastLoadedParam !== paramValue) ecsComponent[paramName] = paramValue;
53-
});
54-
reg.addComponent(ecsEntity, ecsComponent);
55-
});
56-
});
57-
this.lastLoadedSave = JSON.parse(JSON.stringify(save));
43+
this.reloadEvent(save, false);
5844
}
5945

6046
public hardReloadEvent(save: Save): void {
47+
this.reloadEvent(save, true);
48+
}
49+
50+
public pauseGameEvent(): void {
51+
this._isPaused = true;
52+
}
53+
public unpauseGameEvent(): void {
54+
this._isPaused = false;
55+
}
56+
57+
public stopGameEvent(): void {
58+
this.core.getExecutionContext().application.setIsRunning(false);
59+
}
60+
61+
private reloadEvent(save: Save, hard: boolean): void {
6162
const reg = this.ecsLibrary.registry;
62-
this.lastLoadedSave = JSON.parse(JSON.stringify(save));
6363
save.entities.forEach(({ id, components }) => {
6464
Object.entries(components).forEach(([componentName, params]) => {
6565
const ogComponent = save.components.find(({ name }) => name === componentName);
@@ -71,22 +71,25 @@ export class CoreEditor {
7171
name: componentName,
7272
});
7373
Object.entries(params).forEach(([paramName, paramValue]) => {
74-
ecsComponent[paramName] = paramValue;
74+
if (!hard) {
75+
const lastLoadedParam = this.lastLoadedSave.entities.find((e) => e.id === id)
76+
?.components[componentName]?.[paramName];
77+
if (lastLoadedParam === paramValue) return;
78+
}
79+
80+
const ogParam = ogComponent.paramsNames.find(
81+
(param) => param === paramName || param === `__RESERVED_ASSET_${paramName}`,
82+
);
83+
if (!ogParam) return;
84+
85+
ecsComponent[paramName] = ogParam.startsWith("__RESERVED_ASSET_")
86+
? this.assetLibrary.getAsset(paramValue)
87+
: paramValue;
7588
});
7689
reg.addComponent(ecsEntity, ecsComponent);
7790
});
7891
});
79-
}
80-
81-
public pauseGameEvent(): void {
82-
this._isPaused = true;
83-
}
84-
public unpauseGameEvent(): void {
85-
this._isPaused = false;
86-
}
87-
88-
public stopGameEvent(): void {
89-
this.core.getExecutionContext().application.setIsRunning(false);
92+
this.lastLoadedSave = JSON.parse(JSON.stringify(save));
9093
}
9194

9295
private getEntityFromEntityId(entityId: string): Entity {

packages/core-editor/test/editor-feature.spec.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { type IRunOptions } from "@nanoforge-dev/common";
2-
import { type ECSClientLibrary } from "@nanoforge-dev/ecs-client";
32
import { afterEach, describe, expect, it, vi } from "vitest";
43

4+
import { type ApplicationConfig } from "../../core/src/application/application-config";
55
import { CoreEvents } from "../src/common/context/events/core-events";
66
import { type Save, type SaveComponent, type SaveEntity } from "../src/common/context/save.type";
77
import { type Core } from "../src/core/core";
@@ -24,7 +24,10 @@ describe("EditorFeatures", () => {
2424
new CoreEditor(
2525
{} as unknown as Core,
2626
{ coreEvents: events, save: { libraries: [] } } as unknown as IRunOptions["editor"],
27-
{} as ECSClientLibrary,
27+
{
28+
getAssetManagerLibrary: () => ({ library: {} }),
29+
getComponentSystemLibrary: () => ({ library: {} }),
30+
} as ApplicationConfig,
2831
).runEvents();
2932
expect(spyHotReload).toHaveBeenCalledTimes(2);
3033
});
@@ -126,7 +129,12 @@ describe("EditorFeatures", () => {
126129
} as any as Save,
127130
coreEvents: events,
128131
} as any as IRunOptions["editor"],
129-
{ registry: fakeReg } as any as ECSClientLibrary,
132+
{
133+
getAssetManagerLibrary: () => ({
134+
library: { getAsset: vi.fn((name: string) => ({ path: name })) },
135+
}),
136+
getComponentSystemLibrary: () => ({ library: { registry: fakeReg } }),
137+
} as unknown as ApplicationConfig,
130138
).hotReloadEvent({ components, entities } as any as Save);
131139
expect(fakeReg.getComponents).toHaveBeenCalledWith({ name: "__RESERVED_entityId" });
132140
expect(getIndex).toHaveBeenNthCalledWith(1, {
Lines changed: 51 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,82 @@
11
import {
2-
type EventTypeEnum,
32
type IEventEmitter,
43
type ListenerType,
4+
type QueuedEvent,
55
} from "../../src/common/context/event-emitter.type";
66

7-
export class EventEmitter implements IEventEmitter {
8-
public listeners: Record<EventTypeEnum | string, ListenerType[]> = {};
9-
public eventQueue: { event: EventTypeEnum | string; args: any[] }[] = [];
7+
export class EventEmitter<
8+
Events extends string,
9+
EventsMap extends Record<Events, unknown[]>,
10+
> implements IEventEmitter<Events, EventsMap> {
11+
public listeners: {
12+
[K in keyof EventsMap]?: ListenerType<Events, EventsMap, K>[];
13+
} = {};
1014

11-
public runEvents = () => {
12-
this.eventQueue.forEach(({ event, args }) => {
13-
this.listeners[event]?.forEach((listener) => {
14-
listener(...args);
15-
});
15+
public eventQueue: QueuedEvent<EventsMap>[] = [];
16+
private readonly _dequeueOnEmit: boolean;
17+
18+
constructor(dequeueOnEmit = false) {
19+
this._dequeueOnEmit = dequeueOnEmit;
20+
}
21+
22+
runEvents(): void {
23+
this.eventQueue.forEach((e) => {
24+
this._executeEvent(e);
1625
});
17-
this.eventQueue = [];
18-
};
1926

20-
public emitEvent(event: EventTypeEnum | string, ...args: any[]) {
21-
this.eventQueue.push({ event, args });
27+
this.eventQueue = [];
2228
}
2329

24-
public addListener(event: EventTypeEnum | string, listener: ListenerType): void {
30+
emitEvent<K extends keyof EventsMap>(event: K, ...args: EventsMap[K]): void {
31+
this.eventQueue.push({
32+
event,
33+
args,
34+
});
35+
if (this._dequeueOnEmit) this.runEvents();
36+
}
37+
addListener<K extends keyof EventsMap>(
38+
event: K,
39+
listener: ListenerType<Events, EventsMap, K>,
40+
): void {
2541
if (!this.listeners[event]) this.listeners[event] = [];
2642
this.listeners[event].push(listener);
2743
}
28-
public on(event: EventTypeEnum | string, listener: ListenerType): void {
44+
on<K extends keyof EventsMap>(event: K, listener: ListenerType<Events, EventsMap, K>): void {
2945
this.addListener(event, listener);
3046
}
3147

32-
public removeListener(event: EventTypeEnum | string, listener: ListenerType): void {
48+
removeListener<K extends keyof EventsMap>(
49+
event: K,
50+
listener: ListenerType<Events, EventsMap, K>,
51+
): void {
3352
if (!this.listeners[event]) return;
3453
const index = this.listeners[event].indexOf(listener);
3554
if (index >= 0) {
3655
this.listeners[event].splice(index, 1);
3756
}
3857
}
39-
public off(event: EventTypeEnum | string, listener: ListenerType): void {
58+
off<K extends keyof EventsMap>(event: K, listener: ListenerType<Events, EventsMap, K>): void {
4059
this.removeListener(event, listener);
4160
}
4261

43-
public removeListenersForEvent(event: EventTypeEnum | string): void {
62+
removeListenersForEvent(event: keyof EventsMap): void {
4463
if (!this.listeners[event]) return;
4564
this.listeners[event] = [];
4665
}
47-
public removeAllListeners(): void {
66+
removeAllListeners(): void {
4867
this.listeners = {};
4968
}
69+
70+
private _executeEvent<K extends keyof EventsMap>({
71+
event,
72+
args,
73+
}: QueuedEvent<EventsMap, K>): void {
74+
this.listeners[event]?.forEach((listener) => {
75+
try {
76+
listener(...args);
77+
} catch (error) {
78+
console.error(`Error handling event [${String(event)}]:`, error);
79+
}
80+
});
81+
}
5082
}

packages/ecs-lib/src/editor-manifest.type.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ type ECSNumberElement = ECSElementDefaults<"number", number>;
6161
*/
6262
type ECSBooleanElement = ECSElementDefaults<"boolean", boolean>;
6363

64+
/**
65+
* * Editor Component Asset Element
66+
* Type for asset element
67+
*/
68+
type ECSAssetElement = ECSElementDefaults<"asset", string>;
69+
6470
/**
6571
* * Editor Component Array Element
6672
* Type for array element
@@ -88,7 +94,12 @@ type ECSObjectElement = {
8894
* Type for component element
8995
*/
9096
type ECSElement =
91-
ECSStringElement | ECSNumberElement | ECSBooleanElement | ECSArrayElement | ECSObjectElement;
97+
| ECSStringElement
98+
| ECSNumberElement
99+
| ECSBooleanElement
100+
| ECSAssetElement
101+
| ECSArrayElement
102+
| ECSObjectElement;
92103

93104
/**
94105
* Manifest for a component to be used in the NanoForge Editor

0 commit comments

Comments
 (0)