-
Notifications
You must be signed in to change notification settings - Fork 0
API Factory
Factory compiles entity / FSM modules into runtime classes, owns the compiled-class registry, creates live entities and state machines, and wires those objects into registry, replication, pooling, and persistence systems.
Important
This page documents FSM/Orchestrator/Core/Factory/init.luau, plus Registry.luau and the persistence wiring used during creation.
Factory has two big jobs:
-
Compilation - turn module definitions into subclasses built on
BaseEntityorBaseStateMachine - Creation - instantiate, reuse, register, replicate, persist, and clean up runtime objects
[Folder / ModuleScript] βββ [CompileModule] βββ [CompileClass] βββ [CompiledClasses]
βββ [CreateEntity / CreateStateMachine] βββ [Registry]
βββ [Replication / Persistence / Cleanup wiring]
| Member | Type | Purpose |
|---|---|---|
Factory.Registry |
module | Active instance lookup and entity pooling |
Factory.BehaviorTree |
module | Behavior-tree helpers initialized with Factory |
Factory.PersistenceManager |
module | Server-only persistence facade |
Factory.BaseModules.BaseEntity |
module | Base entity class |
Factory.BaseModules.BaseStateMachine |
module | Base FSM class |
Factory.Initialize(Util: any) -> ()Initializes base modules and scans the game tree for recognized folders.
- initializes
BaseEntity - initializes
BaseStateMachine - initializes
BehaviorTree - initializes
Registry - initializes
PersistenceManager - scans
game:GetDescendants()for folders namedEntity,Entities,StateMachine,StateMachines,SM, orFSM - calls
CompileFolder(...)for each match
-
@reads:game:GetDescendants() -
@writes: module utility/logger references, compiled-class tables, initialized child modules
Factory.Get(typeName: string, name: string) -> anyReturns a compiled class by name.
| Parameter | Type | Details |
|---|---|---|
typeName |
string |
"Entity", "Entities", "StateMachine", or "StateMachines"
|
name |
string |
Compiled class name |
- The compiled subclass table, or
nilafter logging if missing.
-
@reads:CompiledClasses -
@writes: none
Factory.GetAll(typeName: string) -> {[string]: any}Returns the compiled class map for a category.
- Shared table of compiled classes for that category.
Caution
GetAll() returns the shared category table. Do not mutate it.
CompileModule(child: ModuleScript, typeName: string, classBase: any) -> ()This helper is local to Factory/init.luau, not exported, but it is the core unit of class compilation.
require(child)- if the returned table has
StaticInit, call it withUtility - otherwise, if it has
Initialize, call that for backward compatibility - choose a storage bucket (
EntitiesorStateMachines) - call
CompileClass(...) - store the compiled subclass under
CompiledClasses[bucket][className]
- a bad
requirelogs an error and does not stop the rest of Factory boot - a failing
StaticInit/Initializelogs an error and compilation still proceeds if possible - a compile failure logs and skips only that module
-
@reads: module return table, optionalStaticInit/Initialize -
@writes:CompiledClasses
Factory.CompileFolder(configurationFolder: Folder) -> ()Compiles all ModuleScript children inside a recognized entity/FSM folder and all nested folders beneath it.
| Kind | Accepted names |
|---|---|
| Entity folders |
Entity, Entities
|
| State machine folders |
StateMachine, StateMachines, SM, FSM
|
- Nothing.
-
@reads:configurationFolder:GetDescendants(), child module contents -
@writes:CompiledClasses
local folder = game.ServerScriptService:WaitForChild("FSM")
FSM.Orchestrator.Factory.CompileFolder(folder)- The folder name determines whether
BaseEntityorBaseStateMachineis used. - Nested folders are supported, but only
ModuleScriptchildren are compiled.
Factory exposes Factory.Registry directly.
| Method | Summary |
|---|---|
RegisterEntity(id, entity) |
add live entity |
UnregisterEntity(id) |
remove live entity |
GetEntity(id) |
read live entity |
GetAllEntities() |
cloned live entity map |
PoolEntity(className, entity) |
push entity into per-class pool (max 64) |
GetPooledEntity(className) |
pop pooled entity |
RegisterStateMachine(id, sm) |
add live FSM |
UnregisterStateMachine(id) |
remove live FSM |
GetStateMachine(id) |
read live FSM |
GetAllStateMachines() |
cloned live FSM map |
Entity pooling is class-name based. When CreateEntity() runs, it first asks Registry.GetPooledEntity(className).
If a pooled entity exists, Factory resets all runtime state before reuse:
- bumps
_lifecycleToken - marks
IsValid = true - clears committed data, pending data,
_pendingKeys, and_cache - clears context and re-seeds
EntityId - resets
Version, lock state, persistence flags, cleanup tasks, and version history - destroys old
StateUpdated/Destroyedsignals and creates fresh ones - replays
InitialDatainto committedData - reruns subclass
GetContext(...)if present
Factory.CreateEntity(params: {
EntityClass: any,
EntityId: string?,
Context: {[any]: any}?,
Persistent: boolean?,
PersistenceKey: string?,
InitialData: {[string]: any}?,
}) -> any?Creates or reuses an entity instance.
| Field | Type | Required | Default | Details |
|---|---|---|---|---|
EntityClass |
`string | class` | yes | β |
EntityId |
string? |
no | generated GUID | Unique live entity ID. Also injected into Context.EntityId. |
Context |
table? |
no | {} |
Cloned before use so caller mutations after creation do not retroactively alter Factory's local construction table. |
Persistent |
boolean? |
no |
nil / falsey |
Server-side persistence toggle. |
PersistenceKey |
string? |
no | EntityId |
DataStore key used by PersistenceManager.LoadState(...). |
InitialData |
table? |
no | nil |
Initial replicated/seed data, including optional _v version stamp. |
- Entity instance on success
- existing entity on duplicate ID
-
nilif class resolution or construction fails
-
@reads: compiled classes, registry, entity pool, persistence manager, network manager -
@writes: registry, pooled entity state, entity lifecycle connections, persistence context, remote broadcasts
If Registry.GetEntity(entityId) already returns an entity, Factory logs a warning and returns that existing instance.
Important consequences:
- no new entity is created
- new
Context,InitialData,Persistent, and other params are not applied - this is intentional de-duplication, not an exception path
When a new or pooled entity is accepted, Factory wires:
-
replicated create broadcast - sends
OnEntityCreatedplus sanitized replicatedInitialData -
StateUpdatedlistener - filters replicated keys by schemaReplicate, optionally batches byReplication.RateLimit, and broadcasts deltas -
persistence autosave - if persistent, defers
PersistenceManager.SaveState(entity)after updates -
Destroyedlistener - optionally saves one last time, broadcastsOnEntityDestroyed, unregisters entity commands, and unregisters the entity fromRegistry
Only fields with Schema[key].Replicate == true are sent to clients.
If the class definition contains:
Replication = {
Enabled = true,
RateLimit = 10,
}then replicated changes are buffered and sent at most 10 times per second.
local entity = FSM.Orchestrator.Factory.CreateEntity({
EntityClass = "DoorEntity",
EntityId = "door:front",
Context = {
OwnerId = "front-door",
Model = workspace.FrontDoor,
},
Persistent = true,
PersistenceKey = "world/doors/front",
InitialData = {
IsOpen = false,
Health = 100,
_v = 3,
},
})local DoorEntityClass = FSM.Orchestrator.Factory.Get("Entity", "DoorEntity")
local entity = FSM.Orchestrator.Factory.CreateEntity({
EntityClass = DoorEntityClass,
Context = { OwnerId = "front-door" },
})[Resolve class]
ββββ {Duplicate EntityId?}
βββ yes β [Warn and return existing]
βββ no β {Pooled instance available?}
βββ yes β [Reset pooled instance] βββ
βββ no β [Construct new entity] βββ΄βββ [RegisterEntity]
ββββ [Optional LoadState]
ββββ [Wire StateUpdated / Destroyed]
ββββ [Broadcast OnEntityCreated]
Factory.CreateStateMachine(params: {
StateMachineClass: any,
StateMachineId: string?,
Context: {[any]: any}?,
AutoStart: boolean?,
InitialState: string?,
}) -> any?Creates, registers, and optionally auto-starts an FSM.
| Field | Type | Required | Default | Details |
|---|---|---|---|---|
StateMachineClass |
`string | class` | yes | β |
StateMachineId |
string? |
no | generated GUID | Unique FSM ID. Also written into Context.StateMachineId. |
Context |
table? |
no | {} |
Cloned before construction. After instantiation Factory injects Context.FSM = newSM. |
AutoStart |
boolean? |
no | true |
If not false, Factory immediately calls newSM:Start(...). |
InitialState |
string? |
no | subclass default | Optional runtime override forwarded to Start({ State = InitialState }). |
- FSM instance on success
- existing FSM on duplicate ID
-
nilif class resolution, construction, or auto-start fails
-
@reads: compiled classes, registry, network manager -
@writes: registry, FSM context, cleanup connections, optional network replication listener
If an FSM with the same StateMachineId already exists, Factory logs a warning and returns the existing instance. New context and start params are ignored.
Factory connects all three terminal signals:
CompletedFailedCancelled
All three point at the same cleanup closure, which:
- unregisters the FSM if it is still the active registry entry for that ID
- disconnects the local signal connections
- calls
newSM:Destroy()
If AutoStart ~= false and the FSM has a Start method:
- Factory optionally builds
startParams = { State = InitialState } - calls
Start()insidepcall - if startup fails, cleans up and returns
nil - if startup finishes with
IsActive == false, cleans up and returnsnil
On the server, if Utility.Settings.ServiceManager.Enabled is truthy, Factory also listens to StateChanged and broadcasts:
BroadcastEntityCommand("SyncClient", smId, "ApplyStateMachineStateChanged", newState)That is used by ServiceManager / client tooling rather than by gameplay FSM logic itself.
local fsm = FSM.Orchestrator.Factory.CreateStateMachine({
StateMachineClass = "DoorFSM",
StateMachineId = "door-fsm:front",
Context = {
EntityId = "door:front",
DoorModel = workspace.FrontDoor,
},
AutoStart = true,
InitialState = "Closed",
})local fsm = FSM.Orchestrator.Factory.CreateStateMachine({
StateMachineClass = "QuestFSM",
StateMachineId = "quest:daily-1",
Context = { QuestId = "daily-1" },
AutoStart = false,
})
fsm:Start({ State = "Pending" })[Resolve class]
ββββ {Duplicate StateMachineId?}
βββ yes β [Warn and return existing]
βββ no β [Instantiate FSM]
β [Inject Context.FSM + StateMachineId]
β [RegisterStateMachine]
β [Wire Completed / Failed / Cancelled cleanup]
β {AutoStart?}
βββ yes β [Start] β {Still active?}
β βββ yes β [Return FSM]
β βββ no β [Cleanup and return nil]
βββ no β [Return inactive FSM]
| API | Duplicate key | Behavior |
|---|---|---|
CreateEntity |
EntityId |
warn + return existing entity |
CreateStateMachine |
StateMachineId |
warn + return existing FSM |
This means callers should treat IDs as identity, not merely labels.
local folder = game.ServerScriptService:WaitForChild("Entities")
FSM.Orchestrator.Factory.CompileFolder(folder)
local door = FSM.Orchestrator.Factory.CreateEntity({
EntityClass = "DoorEntity",
EntityId = "door:server-room",
Context = { Model = workspace.ServerRoomDoor },
})local liveDoor = FSM.Orchestrator.Factory.Registry.GetEntity("door:server-room")
local allMachines = FSM.Orchestrator.Factory.Registry.GetAllStateMachines()-- Pooling is usually triggered by Orchestrator.PoolEntity(...).
-- Factory.CreateEntity(...) will automatically reuse a pooled instance
-- when the class name matches.
local reused = FSM.Orchestrator.Factory.CreateEntity({
EntityClass = "ProjectileEntity",
EntityId = "projectile:42",
Context = { OwnerId = "enemy-1" },
})-
CompileModuleis internal. Document it as part of the pipeline, but callCompileFolder()orInitialize()from game code. -
GetAll()returns the shared compiled-class map. Treat it as read-only. -
Entity pooling resets signals. If you cached an old
StateUpdatedconnection from a pooled instance, it will not survive reuse. -
InitialData._vmatters. It seeds entity version counters so later replication packets are not treated as ancient. -
Persistence is server-only.
Persistent = truehas no effect on the client. -
CreateEntityclonesContext, not nested sub-tables. Deep mutable objects are still shared by reference. -
Entity
OnEntityCreatedbroadcasts only replicated fields. Non-replicated persistent or secret fields are intentionally stripped out. -
An FSM created with
AutoStart = falseis inert until:Start()is called. NoOnEnter, no scheduler activity. - Factory cleanup destroys FSMs after terminal signals. Do not expect a completed FSM to remain registered for later inspection unless some external tool captured it first.
- One bad module does not stop the rest of boot. Factory logs and continues compiling other folders/modules.
Quick Links: Home Β· Quick Start Β· API Reference Β· Architecture Β· Examples Β· Glossary
Copyright: Β© 2026 RBXStateMachine contributors Β· Repository Β· License information