Skip to content

Registration Guide

iKryptonic edited this page May 1, 2026 · 12 revisions

Registration Guide

How Orchestrator:RegisterComponents() discovers, compiles, and loads your Entity and StateMachine modules in the current runtime.

Important

This page is based on FSM/Orchestrator/init.luau, FSM/Orchestrator/Core/Factory/init.luau, BaseEntity.luau, and BaseStateMachine.luau.


1. What registration actually does

In the current framework, registration is not a manual list of classes.

local Orchestrator = require(game:GetService("ReplicatedStorage"):WaitForChild("Orchestrator"))
Orchestrator:RegisterComponents()

That single call is responsible for booting the runtime and preparing all discovered components.

Internally, it performs the following high-level sequence:

  1. initializes logger/shared globals/network/factory/scheduler
  2. starts the shared scheduler
  3. disables the base FSM internal loop and routes updates through GlobalStateMachineHeartbeat
  4. scans the game for folders whose names match Entity/FSM aliases
  5. requires each discovered ModuleScript
  6. compiles the module's returned table into a subclass of BaseEntity or BaseStateMachine
  7. stores the compiled class for later CreateEntity() / CreateStateMachine() calls
  8. registers server snapshot callbacks and client sync hooks

2. Folder discovery rules

The current factory does not require one exact path. It scans all descendants of game and compiles any folder whose name matches one of the supported aliases.

Supported Entity folder names

  • Entity
  • Entities

Supported FSM folder names

  • StateMachine
  • StateMachines
  • SM
  • FSM

So all of these are valid discovery locations:

ReplicatedStorage/Entity/
ReplicatedStorage/StateMachine/
ServerStorage/Entities/
ReplicatedStorage/Combat/FSM/
Workspace/NPCs/StateMachines/

Note

The recommended layout is still ReplicatedStorage/Entity and ReplicatedStorage/StateMachine, because it is the easiest layout for Rojo projects and keeps shared code in one obvious place.

Recommended layout

ReplicatedStorage/
β”œβ”€β”€ Orchestrator/
β”œβ”€β”€ Entity/
β”‚   β”œβ”€β”€ SpinnerEntity.luau
β”‚   └── DoorEntity.luau
└── StateMachine/
β”‚   β”œβ”€β”€ SpinnerFSM.luau
β”‚   └── DoorStateMachine.luau

3. What a discoverable module must return

Each compiled module should return a table definition, not a pre-instantiated object.

3.1 Entity module shape

local SpinnerEntity = {
    Name = "SpinnerEntity",
    Mutable = true,
    Schema = {
        IsSpinning = { Type = "boolean", Default = false, Replicate = true },
        SpinRate = { Type = "number", Default = 180, Replicate = true },
    },
}

function SpinnerEntity:GetContext(params)
    local instance = params.Context and params.Context.Instance
    assert(instance and instance:IsA("BasePart"), "SpinnerEntity requires Context.Instance")
    return {
        Instance = instance,
    }
end

function SpinnerEntity:ApplyChanges(changes)
    if changes.IsSpinning ~= nil then
        self.Instance.Material = changes.IsSpinning and Enum.Material.Neon or Enum.Material.SmoothPlastic
    end
end

return SpinnerEntity

3.2 FSM module shape

local SpinnerFSM = {
    Name = "SpinnerFSM",
    ValidStates = { "Idle", "Spin" },
    InitialState = "Idle",
    TerminalStates = {},
}

function SpinnerFSM:RegisterStates()
    self:AddState("Idle", function(fsm)
        fsm:ScheduleTransition(2, "Spin")
    end, { "Spin" })

    self:AddState("Spin", {
        OnEnter = function(state, fsm)
            fsm.Context.Entity.IsSpinning = true
            fsm.Context.Entity:UpdateEntity()
        end,
        Timeout = 2,
        OnTimeout = "Idle",
    }, { "Idle" })
end

return SpinnerFSM

3.3 What the factory compiles from that table

During compilation, the runtime:

  • clones the module definition (or Definition field, if present)
  • ensures Name exists
  • ensures className exists for FSM subclass creation
  • calls BaseEntity.Extend(...) or BaseStateMachine.Extend(...)
  • mixes in your custom methods onto the resulting subclass

That means the module you write is the class definition, not the live instance.


4. StaticInit and Initialize hooks

CompileModule() currently supports an optional module-level initializer.

Preferred order:

  1. StaticInit(Utility)
  2. fallback to Initialize(Utility) for backward compatibility

Example:

local MyFSM = {
    Name = "MyFSM",
    ValidStates = { "Idle" },
    InitialState = "Idle",
}

function MyFSM.StaticInit(Utility)
    print("Factory utility available:", Utility.Logger)
end

function MyFSM:RegisterStates()
    self:AddState("Idle", function() end)
end

return MyFSM

Use StaticInit when a module needs one-time setup at compile time rather than per-instance construction.

Caution

StaticInit is wrapped in pcall. If it throws, the factory logs the error and continues booting other modules.


5. Bootstrap patterns that work well

5.1 Standard server bootstrap

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Orchestrator = require(ReplicatedStorage:WaitForChild("Orchestrator"))

Orchestrator:RegisterComponents()

local entity = Orchestrator.CreateEntity({
    EntityClass = "SpinnerEntity",
    EntityId = "Spinner_1",
    Context = {
        Instance = workspace:WaitForChild("SpinnerPart"),
    },
})

local fsm = Orchestrator.CreateStateMachine({
    StateMachineClass = "SpinnerFSM",
    StateMachineId = "SpinnerFSM_1",
    Context = {
        Entity = entity,
    },
})

5.2 Standard client bootstrap

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local Orchestrator = require(ReplicatedStorage:WaitForChild("Orchestrator", 30))

Orchestrator:RegisterComponents()

if RunService:IsStudio() then
    Orchestrator:StartServiceManager()
end

5.3 Do not pass parameters to RegisterComponents()

The current public boot path is simply:

Orchestrator:RegisterComponents()

The older β€œregister explicit folders or registries” style is not the current source-of-truth flow.


6. Late-loaded content and CompileFolder()

If you insert or generate modules after initial boot, you can compile an eligible folder manually.

local customFolder = ReplicatedStorage:WaitForChild("BossPack"):WaitForChild("FSM")
Orchestrator.Factory.CompileFolder(customFolder)

This is useful for:

  • feature packs loaded later in the session
  • plug-in style content
  • tests that install fixtures dynamically

CompileFolder() recurses nested folders and compiles every ModuleScript it finds.


7. What happens after compilation

Compilation only makes classes available. Live objects appear when you instantiate them.

[ModuleScript discovered] ──→ [require module] ──→ [StaticInit / Initialize]
        ──→ [Extend BaseEntity / BaseStateMachine] ──→ [Store compiled class]
        ──→ [CreateEntity / CreateStateMachine] ──→ [Register live instance in Registry]

Entity instantiation

CreateEntity():

  • resolves the class by name or table
  • creates a unique ID if you do not provide one
  • returns the existing entity when the ID already exists
  • optionally reuses pooled entities
  • applies InitialData
  • registers the entity in the live registry
  • wires replication if running on the server

FSM instantiation

CreateStateMachine():

  • resolves the class by name or table
  • returns the existing FSM when the ID already exists
  • injects Context.FSM = newSM
  • registers the live machine
  • auto-starts it unless AutoStart = false

8. Common registration mistakes

Mistake: wrong framework path

Current repo default:

require(ReplicatedStorage:WaitForChild("Orchestrator"))

not:

require(ReplicatedStorage.RBXStateMachine)

Mistake: putting modules in arbitrary folder names

ReplicatedStorage/AI/ will not be discovered unless the actual folder name matches one of the supported aliases.

Mistake: returning something other than a table

CompileModule() logs an error if require(module) does not return a table definition.

Mistake: assuming compilation creates live instances

It does not. You still need CreateEntity() and CreateStateMachine().

Mistake: assuming a duplicate ID creates a second instance

It does not. The factory warns and returns the existing object.

Mistake: forgetting Mutable = true

Registration succeeds, but later UpdateEntity() fails on immutable entities.


9. Practical checklist

  • framework is synced to ReplicatedStorage.Orchestrator
  • folders use supported alias names
  • each module returns a table definition
  • Name matches the class name you intend to instantiate
  • entities that commit changes declare Mutable = true
  • server and client both call RegisterComponents()
  • runtime-created content calls CompileFolder() when needed

Related guides

🏠 Home


πŸš€ Getting Started


πŸ—οΈ Architecture


πŸ”§ API Reference


πŸ“š Guides


βš™οΈ Advanced


πŸ› οΈ Development


πŸ“– Reference

Clone this wiki locally