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