diff --git a/src/lib/ecs/system.spec.ts b/src/lib/ecs/system.spec.ts
index 8677481..fe510e5 100644
--- a/src/lib/ecs/system.spec.ts
+++ b/src/lib/ecs/system.spec.ts
@@ -1,9 +1,10 @@
///
import { ScriptContext } from "@rbxts/services";
+
import { Query } from "./query";
import { createEvent } from "./storage/event";
-import { System, SystemManager } from "./system";
+import { StorageObject, System, SystemManager } from "./system";
import { World, WorldOptions } from "./world";
function shallowEquals(a: Array, b: Array): boolean {
@@ -19,20 +20,21 @@ world.flush = (): void => {};
let manager = {} as SystemManager;
-/**
- * Normally these would be initialized by the abstract class, but as we are
- * using a mock here, we need to initialize them ourselves.
- */
-function createSystem(): System {
- const system = {} as System;
- system.dt = 0;
- system.enabled = true;
- system.priority = 0;
- system.storage = [];
- system.onUpdate = (): void => {};
- return system;
+class MockSystem extends System {
+ public dt = 0;
+ public enabled = true;
+ public priority = 0;
+ public storage: Array = [];
+
+ public onUpdate(): void {}
}
+class MockSystem1 extends MockSystem {}
+class MockSystem2 extends MockSystem {}
+class MockSystem3 extends MockSystem {}
+class MockSystem4 extends MockSystem {}
+class MockSystem5 extends MockSystem {}
+
export = (): void => {
beforeEach(() => {
manager = new SystemManager(world);
@@ -50,19 +52,18 @@ export = (): void => {
})();
expect(system).to.be.ok();
- expect(tostring(getmetatable(system))).to.equal("ASystemWithAName");
expect(system.priority).to.equal(1000);
});
it("be able to be scheduled", () => {
let callCount = 0;
- const system = createSystem();
+ const system = new MockSystem();
system.onUpdate = (): void => {
callCount += 1;
};
- void manager.scheduleSystem(system);
+ void manager.scheduleSystem(system as System);
void manager.start();
expect(callCount).to.equal(0);
@@ -74,12 +75,12 @@ export = (): void => {
it("be able to schedule multiple systems", () => {
let callCount = 0;
- const system1 = createSystem();
+ const system1 = new MockSystem();
system1.onUpdate = (): void => {
callCount += 1;
};
- const system2 = createSystem();
+ const system2 = new MockSystem1();
system2.onUpdate = (): void => {
callCount += 1;
};
@@ -94,7 +95,7 @@ export = (): void => {
});
it("be able to configure queries", () => {
- const system = createSystem();
+ const system = new MockSystem();
let query;
system.configureQueries = (world: World): void => {
query = new Query(world).mask;
@@ -110,26 +111,26 @@ export = (): void => {
it("call systems in correct priority order", () => {
const systemOrder: Array = [];
- const system1 = createSystem();
+ const system1 = new MockSystem();
system1.priority = 1;
system1.onUpdate = (): void => {
systemOrder.push(4);
};
- const system2 = createSystem();
+ const system2 = new MockSystem1();
system2.priority = 1000;
system2.onUpdate = (): void => {
systemOrder.push(1);
};
- const system3 = createSystem();
+ const system3 = new MockSystem2();
system3.priority = 25;
system3.onUpdate = (): void => {
systemOrder.push(3);
};
- const system4 = createSystem();
+ const system4 = new MockSystem3();
system4.priority = 100;
system4.onUpdate = (): void => {
systemOrder.push(2);
@@ -149,7 +150,7 @@ export = (): void => {
let callCount = 0;
- const system = createSystem();
+ const system = new MockSystem();
system.executionGroup = tempBindableEvent.Event;
system.onUpdate = (): void => {
callCount += 1;
@@ -169,12 +170,12 @@ export = (): void => {
let callCount = 0;
- const system1 = createSystem();
+ const system1 = new MockSystem();
system1.onUpdate = (): void => {
callCount += 1;
};
- const system2 = createSystem();
+ const system2 = new MockSystem1();
system2.onUpdate = (): void => {
callCount += 10;
};
@@ -203,14 +204,14 @@ export = (): void => {
const event = tempBindableEvent.Event;
- const system1 = createSystem();
+ const system1 = new MockSystem();
system1.priority = 1;
system1.executionGroup = event;
system1.onUpdate = (): void => {
systemOrder.push(3);
};
- const system2 = createSystem();
+ const system2 = new MockSystem1();
system2.priority = 100;
system2.executionGroup = event;
@@ -218,26 +219,26 @@ export = (): void => {
systemOrder.push(1);
};
- const system3 = createSystem();
+ const system3 = new MockSystem2();
system3.priority = 5;
system3.executionGroup = event;
system3.onUpdate = (): void => {
systemOrder.push(2);
};
- const system4 = createSystem();
+ const system4 = new MockSystem3();
system4.priority = 1;
system4.onUpdate = (): void => {
systemOrder.push(6);
};
- const system5 = createSystem();
+ const system5 = new MockSystem4();
system5.priority = 5;
system5.onUpdate = (): void => {
systemOrder.push(5);
};
- const system6 = createSystem();
+ const system6 = new MockSystem5();
system6.priority = 100;
system6.onUpdate = (): void => {
systemOrder.push(4);
@@ -259,20 +260,20 @@ export = (): void => {
it("be able to call systems in order", () => {
const systemOrder: Array = [];
- const system1 = createSystem();
+ const system1 = new MockSystem();
system1.onUpdate = (): void => {
systemOrder.push(3);
};
- const system2 = createSystem();
- system2.after = [system1];
+ const system2 = new MockSystem1();
+ system2.after = [MockSystem];
system2.onUpdate = (): void => {
systemOrder.push(2);
};
- const system3 = createSystem();
- system3.after = [system2];
+ const system3 = new MockSystem2();
+ system3.after = [MockSystem1];
system3.onUpdate = (): void => {
systemOrder.push(1);
};
@@ -288,19 +289,19 @@ export = (): void => {
const systemOrder1: Array = [];
- const system4 = createSystem();
+ const system4 = new MockSystem3();
system4.onUpdate = (): void => {
systemOrder1.push(3);
};
- const system5 = createSystem();
- system5.after = [system4];
+ const system5 = new MockSystem4();
+ system5.after = [MockSystem3];
system5.onUpdate = (): void => {
systemOrder1.push(2);
};
- const system6 = createSystem();
- system6.after = [system4, system5];
+ const system6 = new MockSystem5();
+ system6.after = [MockSystem3, MockSystem4];
system6.onUpdate = (): void => {
systemOrder1.push(1);
};
@@ -321,14 +322,14 @@ export = (): void => {
const event = tempBindableEvent.Event;
- const system1 = createSystem();
+ const system1 = new MockSystem();
system1.priority = 1;
system1.onUpdate = (): void => {};
- const system2 = createSystem();
+ const system2 = new MockSystem1();
system2.executionGroup = event;
system2.priority = 100;
- system2.after = [system1];
+ system2.after = [MockSystem];
system2.onUpdate = (): void => {};
let errored = false;
@@ -348,7 +349,7 @@ export = (): void => {
it("be able to disable and enable a system", () => {
let callCount = 0;
- const system = createSystem();
+ const system = new MockSystem();
system.onUpdate = (): void => {
callCount += 1;
};
@@ -361,11 +362,11 @@ export = (): void => {
bindableEvent.Fire();
expect(callCount).to.equal(1);
- manager.disableSystem(system);
+ manager.disableSystem(MockSystem);
bindableEvent.Fire();
expect(callCount).to.equal(1);
- manager.enableSystem(system);
+ manager.enableSystem(MockSystem);
bindableEvent.Fire();
expect(callCount).to.equal(2);
});
@@ -373,7 +374,7 @@ export = (): void => {
it("be able to unschedule a system", () => {
let callCount = 0;
- const system = createSystem();
+ const system = new MockSystem();
system.onUpdate = (): void => {
callCount += 1;
};
@@ -394,7 +395,7 @@ export = (): void => {
it("not allow yielding in a system", () => {
let callCount = 0;
- const system = createSystem();
+ const system = new MockSystem();
system.onUpdate = (): void => {
callCount += 1;
task.wait();
@@ -412,12 +413,12 @@ export = (): void => {
it("should not be called more than once if a system is scheduled later", () => {
let callCount = 0;
- const system = createSystem();
+ const system = new MockSystem();
system.onUpdate = (): void => {
callCount += 1;
};
- const system2 = createSystem();
+ const system2 = new MockSystem1();
system2.onUpdate = (): void => {
callCount += 1;
};
@@ -435,7 +436,7 @@ export = (): void => {
const tempBindableEvent = new Instance("BindableEvent");
const event = createEvent(tempBindableEvent.Event);
- const system = createSystem();
+ const system = new MockSystem();
system.storage.push(event);
let callCount = 0;
@@ -460,14 +461,14 @@ export = (): void => {
expect(callCount).to.equal(1);
- manager.disableSystem(system);
+ manager.disableSystem(MockSystem);
tempBindableEvent.Fire();
bindableEvent.Fire();
expect(callCount).to.equal(1);
- manager.enableSystem(system);
+ manager.enableSystem(MockSystem);
tempBindableEvent.Fire();
bindableEvent.Fire();
@@ -485,13 +486,13 @@ export = (): void => {
});
it("should not send multiple duplicate errors to the console", () => {
- const system = createSystem();
+ const system = new MockSystem();
system.onUpdate = (): void => {
- throw ("test");
+ throw "test";
};
- manager.scheduleSystem(system);
- manager.start();
+ void manager.scheduleSystem(system);
+ void manager.start();
let errorCount = 0;
diff --git a/src/lib/ecs/system.ts b/src/lib/ecs/system.ts
index eb4229a..622c150 100644
--- a/src/lib/ecs/system.ts
+++ b/src/lib/ecs/system.ts
@@ -11,6 +11,9 @@ export type StorageObject = {
setup: () => void;
};
+export type SystemConstructor = new () => System;
+export type SystemName = string;
+
interface SystemInternal extends System {
/**
* Storage for errors that have occurred recently in the system. This is
@@ -73,7 +76,7 @@ export interface System {
*/
export abstract class System {
/** An optional set of systems that must be executed before this system */
- public after?: Array;
+ public after?: Array;
/** The time since the system was last called. */
public dt = 0;
/**
@@ -142,9 +145,10 @@ export class SystemManager {
private executionDefault: ExecutionGroup;
private executionGroupSignals: Map = new Map();
private executionGroups: Set = new Set();
+ // private systemArgs?: Array;
+ private nameToSystem: Map = new Map();
/** Whether or not the system manager has began execution. */
private started = false;
- // private systemArgs?: Array;
private systems: Array = [];
private systemsByExecutionGroup: Map> = new Map();
private world: World;
@@ -163,7 +167,8 @@ export class SystemManager {
*
* @param system The system that should be disabled.
*/
- public disableSystem(system: System): void {
+ public disableSystem(ctor: SystemConstructor): void {
+ const system = this.getSystem(ctor);
system.enabled = false;
for (const storage of system.storage) {
@@ -181,7 +186,8 @@ export class SystemManager {
*
* @param system The system that should be enabled.
*/
- public enableSystem(system: System): void {
+ public enableSystem(ctor: SystemConstructor): void {
+ const system = this.getSystem(ctor);
system.enabled = true;
for (const storage of system.storage) {
@@ -193,6 +199,49 @@ export class SystemManager {
// do we include dev-only logging for things like this?
}
+ /**
+ * Replaces a system with a new system. This is useful for hot-reloading.
+ * This will not work in non-studio environments. Storage will not persist
+ * between the old and new system, and instead will be cleaned up and set
+ * up again.
+ *
+ * @param oldSystem The system to replace
+ * @param newSystem The system to replace it with
+ */
+ public replaceSystem(oldSystem: System, newSystem: System): void {
+ assert(RunService.IsStudio(), "replaceSystem can only be called in Studio");
+
+ const index = this.systems.indexOf(oldSystem);
+ if (index === -1) {
+ throw `System ${tostring(getmetatable(oldSystem))} not found`;
+ }
+
+ for (const storage of oldSystem.storage) {
+ storage.cleanup();
+ }
+
+ this.nameToSystem.set(tostring(getmetatable(oldSystem)), newSystem);
+ this.systems[index] = newSystem;
+
+ newSystem.configureQueries(this.world);
+ this.setupSystemStorage(newSystem);
+
+ const executionIndex = this.systemsByExecutionGroup
+ .get(oldSystem.executionGroup ?? this.executionDefault)
+ ?.indexOf(oldSystem);
+ if (executionIndex === undefined) {
+ throw `System ${tostring(
+ getmetatable(oldSystem),
+ )} not found in execution group ${tostring(oldSystem.executionGroup)}`;
+ }
+
+ this.systemsByExecutionGroup
+ .get(oldSystem.executionGroup ?? this.executionDefault)
+ ?.remove(executionIndex);
+
+ this.sortSystems(this.systems);
+ }
+
/**
* Schedules an individual system.
*
@@ -234,6 +283,12 @@ export class SystemManager {
}
this.systems.push(system);
+
+ if (this.nameToSystem.has(tostring(getmetatable(system)))) {
+ throw `System ${tostring(getmetatable(system))} has already been scheduled!`;
+ }
+
+ this.nameToSystem.set(tostring(getmetatable(system)), system);
}
this.sortSystems(systems);
@@ -346,6 +401,7 @@ export class SystemManager {
public unscheduleSystems(systems: Array): void {
for (const system of systems) {
this.systems.remove(this.systems.indexOf(system));
+ this.nameToSystem.delete(tostring(getmetatable(system)));
const systemInExecutionGroup = this.systemsByExecutionGroup.get(
system.executionGroup ?? this.executionDefault,
@@ -429,6 +485,22 @@ export class SystemManager {
}
}
+ /**
+ * Given a system constructor, returns the system instance if it exists.
+ * @param ctor The system constructor.
+ * @returns The system instance, or undefined if it doesn't exist.
+ */
+ private getSystem(ctor: SystemConstructor): System {
+ const systemName = tostring(ctor);
+ print(systemName);
+ const systemInstance = this.nameToSystem.get(systemName);
+ if (!systemInstance) {
+ throw `System ${systemName} does not exist!`;
+ }
+
+ return systemInstance;
+ }
+
/**
* Initializes a given system.
*
@@ -459,8 +531,11 @@ export class SystemManager {
this.validateSystems(unscheduledSystems);
unscheduledSystems.sort((a, b) => {
- if (a.after !== undefined && a.after.includes(b)) {
- if (b.after !== undefined && b.after.includes(a)) {
+ if (a.after !== undefined && a.after.includes(getmetatable(b) as SystemConstructor)) {
+ if (
+ b.after !== undefined &&
+ b.after.includes(getmetatable(a) as SystemConstructor)
+ ) {
throw error(
`Systems ${tostring(getmetatable(a))} and ${tostring(
getmetatable(b),
@@ -470,7 +545,7 @@ export class SystemManager {
return false;
}
- if (b.after !== undefined && b.after.includes(a)) {
+ if (b.after !== undefined && b.after.includes(getmetatable(a) as SystemConstructor)) {
return true;
}
@@ -587,9 +662,10 @@ export class SystemManager {
}
for (const afterSystem of system.after) {
- if (system.executionGroup !== afterSystem.executionGroup) {
+ const afterSystemInstance = this.getSystem(afterSystem);
+ if (system.executionGroup !== afterSystemInstance.executionGroup) {
const msg = `System ${tostring(getmetatable(system))} and ${tostring(
- getmetatable(afterSystem),
+ afterSystem,
)} are in different execution groups`;
throw msg;
}
diff --git a/src/lib/ecs/world.ts b/src/lib/ecs/world.ts
index 5ef54aa..a34973f 100644
--- a/src/lib/ecs/world.ts
+++ b/src/lib/ecs/world.ts
@@ -21,7 +21,7 @@ import { Observer } from "./observer";
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { ANY, NOT } from "./query";
import { ALL, Query, RawQuery } from "./query";
-import { ExecutionGroup, System, SystemManager } from "./system";
+import { ExecutionGroup, System, SystemConstructor, SystemManager } from "./system";
export interface WorldOptions {
/**
@@ -237,12 +237,12 @@ export class World {
* this can be used for systems that are expected to be reenabled at a
* later point.
*
- * @param system The system that should be disabled.
+ * @param ctor The constructor of the system that should be enabled.
*
* @returns The world instance to allow for method chaining.
*/
- public disableSystem(system: System): this {
- this.scheduler.disableSystem(system);
+ public disableSystem(ctor: SystemConstructor): this {
+ this.scheduler.disableSystem(ctor);
return this;
}
@@ -253,12 +253,12 @@ export class World {
* This will not error if a system that is already enabled is enabled
* again.
*
- * @param system The system that should be enabled.
+ * @param ctor The constructor of the system that should be enabled.
*
* @returns The world instance to allow for method chaining.
*/
- public enableSystem(system: System): this {
- this.scheduler.enableSystem(system);
+ public enableSystem(ctor: SystemConstructor): this {
+ this.scheduler.enableSystem(ctor);
return this;
}
@@ -469,6 +469,19 @@ export class World {
return this.removeComponent(entityId, tag as unknown as AnyComponent);
}
+ /**
+ * Replaces a system with a new system. This is useful for hot-reloading.
+ * This will not work in non-studio environments. Storage will not persist
+ * between the old and new system, and instead will be cleaned up and set
+ * up again.
+ *
+ * @param oldSystem The system to replace
+ * @param newSystem The system to replace it with
+ */
+ public replaceSystem(oldSystem: System, newSystem: System): void {
+ this.scheduler.replaceSystem(oldSystem, newSystem);
+ }
+
/**
* Schedules an individual system to be executed in the world.
*