Skip to content

API Factory

iKryptonic edited this page May 1, 2026 · 8 revisions

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.


Overview

Factory has two big jobs:

  1. Compilation - turn module definitions into subclasses built on BaseEntity or BaseStateMachine
  2. Creation - instantiate, reuse, register, replicate, persist, and clean up runtime objects

High-level pipeline

[Folder / ModuleScript] ──→ [CompileModule] ──→ [CompileClass] ──→ [CompiledClasses]
                       ──→ [CreateEntity / CreateStateMachine] ──→ [Registry]
                       ──→ [Replication / Persistence / Cleanup wiring]

Exposed members

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

Initialization and Lookup

Factory.Initialize(Util)

Factory.Initialize(Util: any) -> ()

Initializes base modules and scans the game tree for recognized folders.

What it does

  1. initializes BaseEntity
  2. initializes BaseStateMachine
  3. initializes BehaviorTree
  4. initializes Registry
  5. initializes PersistenceManager
  6. scans game:GetDescendants() for folders named Entity, Entities, StateMachine, StateMachines, SM, or FSM
  7. calls CompileFolder(...) for each match

Side effects

  • @reads: game:GetDescendants()
  • @writes: module utility/logger references, compiled-class tables, initialized child modules

Factory.Get(typeName, name)

Factory.Get(typeName: string, name: string) -> any

Returns a compiled class by name.

Parameters

Parameter Type Details
typeName string "Entity", "Entities", "StateMachine", or "StateMachines"
name string Compiled class name

Returns

  • The compiled subclass table, or nil after logging if missing.

Side effects

  • @reads: CompiledClasses
  • @writes: none

Factory.GetAll(typeName)

Factory.GetAll(typeName: string) -> {[string]: any}

Returns the compiled class map for a category.

Returns

  • Shared table of compiled classes for that category.

Caution

GetAll() returns the shared category table. Do not mutate it.


Compilation Pipeline

CompileModule(child, typeName, classBase) (internal helper)

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.

What it does

  1. require(child)
  2. if the returned table has StaticInit, call it with Utility
  3. otherwise, if it has Initialize, call that for backward compatibility
  4. choose a storage bucket (Entities or StateMachines)
  5. call CompileClass(...)
  6. store the compiled subclass under CompiledClasses[bucket][className]

Error handling

  • a bad require logs an error and does not stop the rest of Factory boot
  • a failing StaticInit / Initialize logs an error and compilation still proceeds if possible
  • a compile failure logs and skips only that module

Side effects

  • @reads: module return table, optional StaticInit / Initialize
  • @writes: CompiledClasses

Factory.CompileFolder(folder)

Factory.CompileFolder(configurationFolder: Folder) -> ()

Compiles all ModuleScript children inside a recognized entity/FSM folder and all nested folders beneath it.

Recognized folder aliases

Kind Accepted names
Entity folders Entity, Entities
State machine folders StateMachine, StateMachines, SM, FSM

Returns

  • Nothing.

Side effects

  • @reads: configurationFolder:GetDescendants(), child module contents
  • @writes: CompiledClasses

Example

local folder = game.ServerScriptService:WaitForChild("FSM")
FSM.Orchestrator.Factory.CompileFolder(folder)

Gotchas

  • The folder name determines whether BaseEntity or BaseStateMachine is used.
  • Nested folders are supported, but only ModuleScript children are compiled.

Registry Access and Pooling

Factory exposes Factory.Registry directly.

Registry methods most often used with Factory

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

Pooling behavior

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 / Destroyed signals and creates fresh ones
  • replays InitialData into committed Data
  • reruns subclass GetContext(...) if present

Factory.CreateEntity(params)

Factory.CreateEntity(params: {
    EntityClass: any,
    EntityId: string?,
    Context: {[any]: any}?,
    Persistent: boolean?,
    PersistenceKey: string?,
    InitialData: {[string]: any}?,
}) -> any?

Creates or reuses an entity instance.

Parameters

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.

Returns

  • Entity instance on success
  • existing entity on duplicate ID
  • nil if class resolution or construction fails

Side effects

  • @reads: compiled classes, registry, entity pool, persistence manager, network manager
  • @writes: registry, pooled entity state, entity lifecycle connections, persistence context, remote broadcasts

Duplicate-ID behavior

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

Lifecycle wiring performed by Factory

When a new or pooled entity is accepted, Factory wires:

  1. replicated create broadcast - sends OnEntityCreated plus sanitized replicated InitialData
  2. StateUpdated listener - filters replicated keys by schema Replicate, optionally batches by Replication.RateLimit, and broadcasts deltas
  3. persistence autosave - if persistent, defers PersistenceManager.SaveState(entity) after updates
  4. Destroyed listener - optionally saves one last time, broadcasts OnEntityDestroyed, unregisters entity commands, and unregisters the entity from Registry

Replication filter rules

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.

Example

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,
    },
})

Example: class-table input instead of string name

local DoorEntityClass = FSM.Orchestrator.Factory.Get("Entity", "DoorEntity")
local entity = FSM.Orchestrator.Factory.CreateEntity({
    EntityClass = DoorEntityClass,
    Context = { OwnerId = "front-door" },
})

Entity creation flow

[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)

Factory.CreateStateMachine(params: {
    StateMachineClass: any,
    StateMachineId: string?,
    Context: {[any]: any}?,
    AutoStart: boolean?,
    InitialState: string?,
}) -> any?

Creates, registers, and optionally auto-starts an FSM.

Parameters

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 }).

Returns

  • FSM instance on success
  • existing FSM on duplicate ID
  • nil if class resolution, construction, or auto-start fails

Side effects

  • @reads: compiled classes, registry, network manager
  • @writes: registry, FSM context, cleanup connections, optional network replication listener

Duplicate-ID behavior

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.

Lifecycle wiring performed by Factory

Factory connects all three terminal signals:

  • Completed
  • Failed
  • Cancelled

All three point at the same cleanup closure, which:

  1. unregisters the FSM if it is still the active registry entry for that ID
  2. disconnects the local signal connections
  3. calls newSM:Destroy()

Auto-start behavior

If AutoStart ~= false and the FSM has a Start method:

  • Factory optionally builds startParams = { State = InitialState }
  • calls Start() inside pcall
  • if startup fails, cleans up and returns nil
  • if startup finishes with IsActive == false, cleans up and returns nil

State replication behavior

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.

Example

local fsm = FSM.Orchestrator.Factory.CreateStateMachine({
    StateMachineClass = "DoorFSM",
    StateMachineId = "door-fsm:front",
    Context = {
        EntityId = "door:front",
        DoorModel = workspace.FrontDoor,
    },
    AutoStart = true,
    InitialState = "Closed",
})

Example: create inactive, start later

local fsm = FSM.Orchestrator.Factory.CreateStateMachine({
    StateMachineClass = "QuestFSM",
    StateMachineId = "quest:daily-1",
    Context = { QuestId = "daily-1" },
    AutoStart = false,
})

fsm:Start({ State = "Pending" })

FSM creation flow

[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]

Duplicate-ID Policy Summary

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.


Real Examples

Compile, then create

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 },
})

Using Registry through Factory

local liveDoor = FSM.Orchestrator.Factory.Registry.GetEntity("door:server-room")
local allMachines = FSM.Orchestrator.Factory.Registry.GetAllStateMachines()

Pulling pooled entities indirectly through create

-- 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" },
})

Edge Cases & Gotchas

  1. CompileModule is internal. Document it as part of the pipeline, but call CompileFolder() or Initialize() from game code.
  2. GetAll() returns the shared compiled-class map. Treat it as read-only.
  3. Entity pooling resets signals. If you cached an old StateUpdated connection from a pooled instance, it will not survive reuse.
  4. InitialData._v matters. It seeds entity version counters so later replication packets are not treated as ancient.
  5. Persistence is server-only. Persistent = true has no effect on the client.
  6. CreateEntity clones Context, not nested sub-tables. Deep mutable objects are still shared by reference.
  7. Entity OnEntityCreated broadcasts only replicated fields. Non-replicated persistent or secret fields are intentionally stripped out.
  8. An FSM created with AutoStart = false is inert until :Start() is called. No OnEnter, no scheduler activity.
  9. 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.
  10. One bad module does not stop the rest of boot. Factory logs and continues compiling other folders/modules.

Related Pages

🏠 Home


πŸš€ Getting Started


πŸ—οΈ Architecture


πŸ”§ API Reference


πŸ“š Guides


βš™οΈ Advanced


πŸ› οΈ Development


πŸ“– Reference

Clone this wiki locally