diff --git a/src/index.ts b/src/index.ts
index 6e82208..f2a8f91 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,8 +12,6 @@ import { Internals } from "./lib/net/internal";
import { Client } from "./lib/net/util/client";
import { Identifiers } from "./lib/net/util/identifiers";
import { Server } from "./lib/net/util/server";
-import { Process } from "./lib/process/process";
-import Scheduler from "./lib/process/scheduler";
import { Component, ComponentData, Flyweight, FlyweightData, TagComponent } from "./lib/types/ecs";
import { Users } from "./lib/user";
import { DefaultUserDeclaration } from "./lib/user/default/types";
@@ -88,20 +86,6 @@ namespace Tina {
return new TinaCore();
}
- /**
- * Used to add new processes to the processor.
- *
- * @param name process name to add.
- * @returns a Process object.
- */
- export function process(name: string): Process {
- if (Process.processes.has(name)) {
- return Process.processes.get(name)!;
- }
-
- return new Process(name, Scheduler);
- }
-
export const log: Scope = Logger.scope("TINA");
/**
@@ -246,6 +230,10 @@ export { User, Users } from "./lib/user";
/** State namespace */
export { State } from "./lib/state";
+/** Process class and scheduler namespace */
+export { Process } from "./lib/processes/process";
+export { Scheduler } from "./lib/processes/scheduler";
+
/** Container export */
export { Container } from "./lib/container";
diff --git a/src/lib/process/process.spec.ts b/src/lib/process/process.spec.ts
deleted file mode 100644
index ca2b5be..0000000
--- a/src/lib/process/process.spec.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-///
-
-import Tina from "../..";
-import { Process } from "./process";
-import Scheduler from "./scheduler";
-
-export = () => {
- describe("Process", () => {
- it("should create new process", () => {
- const process = new Process("myName", Scheduler);
- expect(process).to.be.ok();
- expect(process.name).to.equal("myName");
- });
-
- it("should return the same Process when using Tina.process() and new Process()", () => {
- const process1 = new Process("shouldMatchProcess", Scheduler);
- const process2 = Tina.process("shouldMatchProcess");
- expect(process1).to.equal(process2);
- });
-
- it("should add to ticker on .resume()", () => {
- const process = new Process("callMeAnything", Scheduler);
- expect(Scheduler.hasProcess(process.name)).to.equal(false);
- process.resume();
- expect(Scheduler.hasProcess(process.name)).to.equal(true);
- Scheduler.removeProcess(process);
- });
-
- it("should suspend on .suspend()", () => {
- const process = new Process("", Scheduler);
- expect(process.isSuspended).to.equal(false);
-
- process.suspend(23);
- expect(process.isSuspended).to.equal(true);
- expect(process.suspensionTime).to.equal(23);
- });
- });
-};
diff --git a/src/lib/process/process.ts b/src/lib/process/process.ts
deleted file mode 100644
index 5e6bc4f..0000000
--- a/src/lib/process/process.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { EventEmitter } from "../events";
-import Scheduler from "./scheduler";
-
-type ProcessScheduler = typeof Scheduler;
-interface Events {
- _default: [dt: number];
-}
-
-export class Process extends EventEmitter {
- public static processes = new Map();
-
- public name: string;
- public isSuspended = false;
- public suspensionTime = -1;
-
- private ticker: ProcessScheduler;
-
- constructor(name: string, ticker: ProcessScheduler) {
- super();
-
- this.name = name;
- this.ticker = ticker;
-
- // Add to static list of all created Processes
- Process.processes.set(name, this);
- }
-
- public resume(): void {
- this.isSuspended = false;
- this.ticker.addProcess(this);
- }
-
- public suspend(ticks = 1): void {
- this.suspensionTime = ticks;
- this.isSuspended = true;
- }
-
- public tick(dt: number): void {
- return this.emit("_default", dt);
- }
-}
diff --git a/src/lib/process/scheduler.spec.ts b/src/lib/process/scheduler.spec.ts
deleted file mode 100644
index de32713..0000000
--- a/src/lib/process/scheduler.spec.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-///
-
-import { Process } from "./process";
-import Scheduler, { ProcessScheduler } from "./scheduler";
-
-export = (): void => {
- describe("Scheduler", () => {
- it("should exist", () => {
- expect(Scheduler).to.be.ok();
- });
-
- it("should be able to add and remove processes from it", () => {
- const ticker = new ProcessScheduler();
- const process = new Process("addAndRemove", ticker);
- expect(ticker.hasProcess(process.name)).to.equal(false);
- ticker.addProcess(process);
- expect(ticker.hasProcess(process.name)).to.equal(true);
- ticker.removeProcess(process);
- expect(ticker.hasProcess(process.name)).to.equal(false);
- });
-
- it("should call unsuspended processes", () => {
- const ticker = new ProcessScheduler();
- ticker.start();
- const process = new Process("unsuspended", ticker);
- ticker.addProcess(process);
-
- let called = false;
- process.when().do(() => (called = true));
- task.wait(0.5);
-
- ticker.destroy();
- expect(called).to.equal(true);
- });
-
- it("should not call suspended processes", () => {
- const ticker = new ProcessScheduler();
- const process = new Process("suspended", ticker);
- ticker.start();
- ticker.addProcess(process);
- process.suspend(1000); // 20 ticks = 1 second at 20 TPS
-
- let called = false;
- process.when().do(() => (called = true));
- task.wait(0.5);
-
- ticker.destroy();
-
- // Should timeout
- expect(called).to.equal(false);
- });
-
- it("should be able to remove process during a tick", () => {
- const ticker = new ProcessScheduler();
- const process1 = new Process("1", ticker);
- const process2 = new Process("2", ticker);
- ticker.start();
-
- let called = false;
- process1.when().do(() => ticker.removeProcess(process1));
- process2.when().do(() => {
- called = true;
- });
-
- process1.resume();
- process2.resume();
-
- task.wait(0.5);
-
- const hasProcess = ticker.hasProcess(process1.name);
- ticker.destroy();
-
- expect(hasProcess).to.equal(false);
- expect(called).to.equal(true);
- });
- });
-};
diff --git a/src/lib/process/scheduler.ts b/src/lib/process/scheduler.ts
deleted file mode 100644
index f12b554..0000000
--- a/src/lib/process/scheduler.ts
+++ /dev/null
@@ -1,112 +0,0 @@
-import { Process } from "./process";
-
-const RunService = game.GetService("RunService");
-
-/**
- *
- */
-export class ProcessScheduler {
- private static TPS = 20; // Grab value from tina.yaml when able
- private lastTick: number;
- private timeBetweenTicks: number;
- private connection?: RBXScriptConnection;
- private processes: Array = new Array();
- private processesToRemove: Array = new Array();
-
- /** Used to keep track of when the .update() method is running since .removeProcess() could be called during a Process callback. */
- private updating = false;
-
- constructor() {
- this.timeBetweenTicks = 1 / ProcessScheduler.TPS;
- this.lastTick = os.clock();
- }
-
- private onHeartbeat(): void {
- const currentTick = os.clock();
- const deltaTime = currentTick - this.lastTick;
- if (deltaTime >= this.timeBetweenTicks) {
- // Adjust lastTick to based on timeBetweenTicks to keep interval relatively stable
- this.lastTick = currentTick - (deltaTime % this.timeBetweenTicks);
- this.update(deltaTime);
- }
- }
-
- private update(dt: number): void {
- // Update has started
- this.updating = true;
-
- for (const process of this.processes) {
- // Update suspended processes
- if (process.isSuspended) {
- process.suspensionTime -= 1;
- if (process.suspensionTime < 0) {
- process.isSuspended = false;
- }
- }
-
- // Run active processes
- if (process.isSuspended) continue;
- try {
- process.tick(dt);
- } catch (error) {
- // TODO: log any errors properly.
- }
- }
-
- // Remove processes in queue
- for (const process of this.processesToRemove) {
- this._removeProcess(process);
- }
- this.processesToRemove = new Array();
-
- // Update has finished
- this.updating = false;
- }
-
- public addProcess(process: Process): void {
- if (this.processes.includes(process)) return;
-
- this.processes.push(process);
- }
-
- public removeProcess(process: Process): void {
- // If Processes are currently being updated then deffer removal until updating has finished
- if (this.updating) {
- this.processesToRemove.push(process);
- } else {
- this._removeProcess(process);
- }
- }
-
- private _removeProcess(process: Process): void {
- const index = this.processes.indexOf(process);
- if (index < 0) return;
- this.processes.remove(index);
- }
-
- public hasProcess(name: string): boolean {
- return !!this.processes.find(p => p.name === name);
- }
-
- public getProcess(name: string): Process | undefined {
- return this.processes.find(p => p.name === name);
- }
-
- public start(): void {
- if (this.connection) return; // TODO: log that they're doing something stupid.
- this.connection = RunService.Heartbeat.Connect(() => this.onHeartbeat());
- }
-
- /**
- * @hidden
- * Remove all references and disconnect all connections
- */
- public destroy(): void {
- this.connection?.Disconnect();
- this.processes = new Array();
- this.processesToRemove = new Array();
- }
-}
-
-const Scheduler = new ProcessScheduler();
-export default Scheduler;
diff --git a/src/lib/processes/process.spec.ts b/src/lib/processes/process.spec.ts
new file mode 100644
index 0000000..6eba91e
--- /dev/null
+++ b/src/lib/processes/process.spec.ts
@@ -0,0 +1 @@
+///
diff --git a/src/lib/processes/process/index.ts b/src/lib/processes/process/index.ts
new file mode 100644
index 0000000..edf9276
--- /dev/null
+++ b/src/lib/processes/process/index.ts
@@ -0,0 +1,52 @@
+import { Scheduler, TinaScheduler } from "../scheduler";
+import { ProcessStatus } from "./types";
+
+export abstract class Process {
+ public scheduler: Scheduler = TinaScheduler;
+
+ constructor() {
+ this.scheduler.schedule(this);
+ }
+
+ /**
+ * Suspends current process by the given ticks (defaults to 1).
+ *
+ * @param ticks amount of ticks.
+ */
+ public suspend(ticks = 1): void {
+ return this.scheduler.suspend(this, ticks);
+ }
+
+ /**
+ * Resumes current process.
+ */
+ public resume(): void {
+ return this.scheduler.unsuspend(this);
+ }
+
+ /**
+ * Returns current process Status.
+ *
+ * @returns a string indicating it's current status.
+ */
+ public status(): ProcessStatus {
+ const isActive = this.scheduler.has(this);
+ const isSuspended = this.scheduler.isSuspended(this);
+
+ return isSuspended ? "suspended" : isActive ? "active" : !isActive ? "dead" : "unknown";
+ }
+
+ /**
+ * Deletes current process, not anymore needed.
+ */
+ public delete(): void {
+ return this.scheduler.unschedule(this);
+ }
+
+ /**
+ * Method invoked every tick (specified on the scheduler)
+ *
+ * @param dt delta time between ticks.
+ */
+ public abstract update(dt: number): void;
+}
diff --git a/src/lib/processes/process/types.d.ts b/src/lib/processes/process/types.d.ts
new file mode 100644
index 0000000..6cf2eb9
--- /dev/null
+++ b/src/lib/processes/process/types.d.ts
@@ -0,0 +1 @@
+export type ProcessStatus = "active" | "dead" | "suspended" | "unknown";
diff --git a/src/lib/processes/scheduler/index.ts b/src/lib/processes/scheduler/index.ts
new file mode 100644
index 0000000..9fc9fe5
--- /dev/null
+++ b/src/lib/processes/scheduler/index.ts
@@ -0,0 +1,143 @@
+import { RunService } from "@rbxts/services";
+
+import { ExecutionGroup } from "../../ecs/system";
+import { ConnectionUtil } from "../../util/connection-util";
+import { Process } from "../process";
+
+export abstract class Scheduler {
+ public executionGroup?: ExecutionGroup;
+ public ticksPerSecond?: number;
+
+ private processes: Map = new Map();
+ private suspended: Map = new Map();
+
+ private stopped = false;
+
+ constructor(configuration?: { executionGroup?: ExecutionGroup; ticksPerSecond?: number }) {
+ this.executionGroup = configuration?.executionGroup ?? RunService.Heartbeat;
+
+ ConnectionUtil.connect(this.executionGroup, (dt: number) => {
+ this.update(dt);
+ });
+ }
+
+ /**
+ * Schedules a new process.
+ *
+ * @param process process to schedule.
+ */
+ public schedule(process: Process): void {
+ return void this.processes.set(process, os.clock());
+ }
+
+ /**
+ * Unschedules a current on-going process, deferred to the end of the current resumption cycle.
+ *
+ * @param process process to unschedule.
+ */
+ public unschedule(process: Process): void {
+ return void task.defer(() => this.processes.delete(process));
+ }
+
+ /**
+ * Checks if a Scheduler has any matching process.
+ *
+ * @param process a process to check on.
+ */
+ public has(process: Process): boolean {
+ return this.processes.has(process);
+ }
+
+ /**
+ * Suspends specified process.
+ *
+ * @param process process to suspend.
+ * @param ticks ticks to suspend the process.
+ */
+ public suspend(process: Process, ticks: number): void {
+ return void this.suspended.set(process, ticks);
+ }
+
+ /**
+ * Unsuspends specified process (to the end of the current resumption cycle).
+ *
+ * @param process process to unsuspend.
+ */
+ public unsuspend(process: Process): void {
+ return void task.defer(() => this.suspended.delete(process));
+ }
+
+ /**
+ * Checks if a process is currently suspended.
+ *
+ * @param process process to check on.
+ */
+ public isSuspended(process: Process): boolean {
+ return this.suspended.has(process);
+ }
+
+ /**
+ * Stops current processes, can be resumed later on.
+ */
+ public stop(): void {
+ this.stopped = true;
+ }
+
+ /**
+ * Resumes already going processes.
+ */
+ public resume(): void {
+ this.stopped = false;
+ }
+
+ /**
+ * Clears all current processes going on.
+ */
+ public clear(): void {
+ return void task.defer(() => this.processes.clear());
+ }
+
+ /**
+ * Updates all available processes.
+ *
+ * @param dt delta time between frames (or ticks)
+ */
+ private update(dt: number): void {
+ if (this.stopped) return;
+
+ for (const [process, lastTick] of this.processes) {
+ let delta = dt;
+
+ if (this.ticksPerSecond !== undefined) {
+ const currentTick = os.clock();
+
+ delta = (currentTick - lastTick) * dt;
+ if (delta < this.ticksPerSecond) return;
+
+ this.processes.set(process, currentTick - (delta % this.ticksPerSecond));
+ }
+
+ if (this.suspended.has(process)) {
+ const leftTicks = this.suspended.get(process)! - 1;
+
+ if (leftTicks > 0) {
+ return void this.suspended.set(process, leftTicks);
+ }
+
+ this.suspended.delete(process);
+ }
+
+ return process.update(delta);
+ }
+ }
+}
+
+class InternalScheduler extends Scheduler {
+ constructor() {
+ super();
+
+ this.executionGroup = RunService.Heartbeat;
+ }
+}
+
+export const TinaScheduler = new InternalScheduler();
diff --git a/src/lib/processes/scheduler/types.d.ts b/src/lib/processes/scheduler/types.d.ts
new file mode 100644
index 0000000..e69de29