Skip to content

API ServiceManager

iKryptonic edited this page May 1, 2026 · 4 revisions

API: ServiceManager

ServiceManager is the framework's developer dashboard: a lazy-started client UI backed by a server snapshot API, a behavior-tree tick loop (ServiceBrain / β€œNexusBrain”), and nine subsystem controllers.

Important

Docs audit note (2026-05): This page was re-audited against FSM/Orchestrator/ServiceManager/init.luau, ServiceBrain.luau, and the subsystem modules.


🎯 Overview

There are two distinct phases:

  1. server registration β€” Orchestrator:RegisterComponents() requires the module and calls ServiceManager.RegisterServerHandlers(self) so clients can request ServiceManager_ServerSnapshot
  2. client launch β€” Orchestrator:StartServiceManager() requires the same module, calls Initialize(self), then Show()
local FSM = require(game:GetService("ReplicatedStorage").RBXStateMachine)
FSM.Orchestrator:RegisterComponents() -- server snapshot endpoint is registered on the server

-- client only
local dashboard = FSM.Orchestrator:StartServiceManager()
dashboard.SwitchSubsystem("NETWORK")

🧩 Subsystems

Initialize() instantiates exactly nine root subsystem modules:

Key Module Purpose
TASKS TasksModule scheduler list, per-task perf, FPS strip
FSM FSMModule live + completed FSMs, graph visualizer, transition history
ENTITY EntityModule entities, schema keys, version history, data map
NETWORK NetworkModule remotes, server callbacks, EventBuses, call history
DATASTORE DataStoreModule DataStoreSystem snapshot + operation visibility
INSIGHTS InsightsModule analytics summaries / recommendations
CONSOLE ConsoleModule console buffer and debug helpers
LOGS LogsModule aggregated logger history
PROFILER ProfilerModule profiler-facing metrics and drill-downs

Root layers are registered into ZAxisNavigator in this same order.


🧠 Lifecycle and high-level flow

  Client          Orchestrator        ServiceManager       Server handlers
    β”‚                  β”‚                    β”‚                    β”‚
    │─ StartServiceManager() ─→│            β”‚                    β”‚
    β”‚                  │─ Initialize(self) ─→│                   β”‚
    β”‚                  β”‚                    β”‚ build store, GUI, navigator, ServiceBrain, subsystems
    β”‚                  β”‚                    β”‚ schedule ServiceManagerBrainTick
    β”‚                  β”‚                    β”‚ start polling loop
    β”‚                  │─ Show() ──────────→│                   β”‚
    β”‚                  β”‚                    │─ Request("ServiceManager_ServerSnapshot") ─→│
    β”‚                  β”‚                    │←────────────── snapshot ────────────────────│
    β”‚                  β”‚                    β”‚ cache _serverData + render active subsystem

πŸ”„ Context model

ServiceManager is explicitly context-aware.

Mode Data source Notes
"client" live client-side Orchestrator objects _SyncClientContext() is currently a no-op because data is already local
"server" cached _serverData returned by ServiceManager_ServerSnapshot server mode forces an immediate sync request on mode switch

Reactive store keys seeded during Initialize():

  • nexus.mode = "client"
  • nexus.connected = true
  • nexus.activeSubsystem = nil

When the mode changes, ServiceManager intentionally discards drill-down layers because their captured Context tables point at stale client-only or server-only objects.


🌳 NexusBrain / ServiceBrain

ServiceBrain.Build() returns the top-level behavior tree used by _TickBrain().

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ _TickBrain β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜
      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ initialized + visible? β”‚
β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
      β”‚ no          β”‚ yes
      β–Ό             β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ return β”‚   β”‚ SyncContextData β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                      β–Ό
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚ NeedsDataRefresh? β”‚
            β””β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
                  β”‚ yes    β”‚ no
                  β–Ό        β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ FetchSubsystemData β”‚  β”‚ HasDirtyUI? β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β””β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”€β”˜
             β–Ό                  β”‚yes β”‚no
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β–Ό    β–Ό
   β”‚ RunInsightAnalysis β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚RenderActiveSubsystemβ”‚ β”‚ UpdateAnimations β”‚
             β–Ό             β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚
   β”‚ RenderActiveSubsystemβ”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              β–Ό
   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
   β”‚ UpdateAnimations β”‚
   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key behavior:

  • _TickBrain() exits immediately if not initialized
  • _TickBrain() also exits while the GUI exists but is hidden
  • root layers always rebuild on refresh; drill-down layers rebuild only when LiveUpdate == true
  • the active subsystem's FetchData is called before render whenever the refresh interval elapses

πŸ“‘ Server snapshot mechanism

RegisterServerHandlers() installs one sync request handler:

Orchestrator.RegisterServerRequestCallback("ServiceManager_ServerSnapshot", function(_player)
    return snapshot
end, false)

Snapshot payload assembled on the server:

Key Shape Source
Tasks { List = {...}, Performance = {...} } Orchestrator.Scheduler
FSM { StateMachines = _fsmShadow } live FSM registry + bounded shadow cache
Entities flat entity map Factory.Registry.GetAllEntities()
Network remotes / commands / buses / callbacks Orchestrator.NetworkManager
DataStore { Stores = {}, Operations = {} } DataStoreSystem entity
Memory collectgarbage("count") KB current server VM
Timestamp os.clock() snapshot time
Logs most recent 200 aggregated log entries orchestrator + scheduler + network + factory loggers

FSM shadow registry

RegisterServerHandlers() maintains a bounded shadow cache so completed / failed FSMs remain inspectable after the Factory unregisters them.

  • SHADOW_CAP = 200
  • _fsmShadow[id] stores serializable FSM snapshots
  • _fsmShadowOrder tracks touch order for FIFO eviction
  • _fsmLiveRefs[id] temporarily keeps the live FSM reference so final completed vs failed status can be inferred after the registry entry disappears
  • StateChanged connections append transition-history copies into the shadow so the graph view survives live FSM destruction

πŸ§ͺ Debug command bridge (ServiceManagerCmd)

On the client, Initialize() watches ReplicatedStorage:GetAttribute("ServiceManagerCmd") and dispatches commands whenever the attribute changes.

Supported commands in the current source:

Command Effect
context:client / context:server calls SetContextMode(...)
subsystem:TASKS (or any other root key) calls SwitchSubsystem(...)
fsmDetail:<fsmId> opens the FSM detail drill-down
fsmViz:<fsmId> opens the FSM graph visualizer
step:<N> sets FSM visualizer playback step
step:live switches FSM visualizer back to live mode
netDetail:<itemName> opens a NETWORK detail drill-down
netCalls:<itemName> opens a NETWORK calls-history drill-down
consolePush:<text> injects a synthetic console line
consoleEnter:<text> simulates entering text in the console input and writes a result string to ServiceManagerResult
dumpCalls:<itemName> dumps the latest ArgsStruct for a NETWORK row into ServiceManagerResult

Example:

local rs = game:GetService("ReplicatedStorage")
rs:SetAttribute("ServiceManagerCmd", "context:server")
rs:SetAttribute("ServiceManagerCmd", "subsystem:NETWORK")
rs:SetAttribute("ServiceManagerCmd", "netCalls:Remote:EntityUpdateEvent")

πŸ”§ API reference

ServiceManager.Initialize(Orchestrator: any) -> ()

One-time client-side setup entry point. Idempotent.

What it creates / schedules

  1. ServiceBrain.Initialize(Orchestrator)
  2. ReactiveStore.new({ ... })
  3. AnimationController.new()
  4. AnalyticsEngine.new()
  5. the 9 subsystem module instances
  6. GlassGUI for Players.LocalPlayer.PlayerGui when on the client
  7. ZAxisNavigator when a GUI body container exists
  8. _brainTree = ServiceBrain.Build()
  9. Orchestrator.Scheduler:Schedule({ TaskName = "ServiceManagerBrainTick", Priority = 15, Event = "Heartbeat", IsRecurringTask = true, TaskExecutionDelay = 0, ... })
  10. a RenderStepped FPS sampler on the client
  11. the background server-poll loop on the client
  12. the ServiceManagerCmd attribute bridge on the client

Side effects

  • sets _initialized = true
  • caches _orchestrator
  • seeds the reactive store
  • writes ServiceManager._initialized = true
  • logs "ServiceManager initialized"

Edge cases

  • second call returns immediately
  • on the server, GUI-specific steps are skipped
  • if Orchestrator.Scheduler is unavailable, no brain tick task is scheduled

ServiceManager.Show() -> ()

Shows the GUI if it exists.

Side effects

  • calls _gui:Show()
  • if no subsystem is active yet, immediately switches to TASKS

ServiceManager.Hide() -> ()

Hides the window without destroying it.

Important

  • once hidden, _TickBrain() early-returns before any BT work or rendering

ServiceManager.Toggle() -> ()

Toggles visibility by delegating to Show() / Hide().


ServiceManager.SwitchSubsystem(name: string) -> ()

Activates one of the nine root subsystems.

Accepted values:

  • TASKS
  • FSM
  • ENTITY
  • NETWORK
  • DATASTORE
  • INSIGHTS
  • CONSOLE
  • LOGS
  • PROFILER

Side effects

  • ignores unknown names
  • sets _activeSubsystem = name
  • writes nexus.activeSubsystem in the store
  • pre-fetches subsystem data via module.FetchData(...) when present
  • navigates the Z-axis root layer
  • updates GUI sidebar selection with callback suppression
  • forces immediate refresh by setting _lastDataRefresh = 0
local sm = FSM.Orchestrator:StartServiceManager()
sm.SwitchSubsystem("FSM")

ServiceManager.SetContextMode(mode: "client" | "server") -> ()

Switches between live client data and cached server snapshot data.

Behavior

  • invalid modes are ignored
  • same-mode calls are ignored
  • updates _contextMode and nexus.mode
  • updates GUI context widgets and status bar
  • stops active animation pulses before tearing down stale drill-downs
  • when entering server, immediately performs _orchestrator.Request("ServiceManager_ServerSnapshot")
  • navigates back to the active subsystem root
  • pushes a context divider line into the console buffer
  • resets the TASKS perf history / smoothing buffers
  • forces the next tick to refetch with _lastDataRefresh = 0
  • logs the context switch

Failure path

  • failed immediate server poll sets nexus.connected = false and logs a warning
sm.SetContextMode("server")

ServiceManager.GetStore() -> any

Returns the underlying ReactiveStore instance.

This is the supported escape hatch for advanced subscribers that need to watch nexus.mode, nexus.connected, or nexus.activeSubsystem.


ServiceManager.Destroy() -> ()

Tears down the dashboard.

Side effects

  • disconnects _serverPollConnection when set
  • destroys ZAxisNavigator
  • stops all animation pulses
  • destroys AnalyticsEngine
  • destroys ReactiveStore
  • destroys GlassGUI
  • calls Destroy() on every subsystem module that exposes it
  • clears _subsystemModules and _logs
  • sets _initialized = false

Important

  • the polling loop exits because it is gated by while _initialized do

ServiceManager.RegisterServerHandlers(Orchestrator: any) -> ()

Server-only snapshot registration hook. Called automatically by Orchestrator:RegisterComponents() on the server.

Behavior

  • returns immediately on the client
  • guarded by ServiceManager._serverHandlersRegistered so repeated registration is a no-op with a warning log
  • seeds the FSM shadow cache from already-registered live FSMs
  • subscribes to Factory.Registry.StateMachineRegistered
  • mirrors state changes into shadow transition history
  • registers sync request ServiceManager_ServerSnapshot
  • logs "ServiceManager server handlers registered"

πŸ–₯️ Polling cadence and UI refresh rules

_StartServerPolling() runs only on the client.

  • requests ServiceManager_ServerSnapshot continuously while _initialized == true
  • sleeps 2 seconds in server mode
  • sleeps 5 seconds in client mode
  • always keeps _serverData warm, even in client mode, so drills can switch contexts quickly

_TickBrain() refresh rules:

  • hidden GUI => no BT work
  • refresh interval defaults to _dataRefreshInterval = 2.0
  • dirty root views rebuild on each refresh
  • drill-down layers rebuild only if they opted into Context.LiveUpdate = true

πŸͺΆ Practical β€œzero-overhead when disabled” model

The current implementation delivers this in a few separate ways rather than one global kill-switch:

  1. lazy start β€” client GUI, polling, navigator, and brain tick do not exist until Orchestrator:StartServiceManager() is called
  2. hidden-window short-circuit β€” once initialized, _TickBrain() returns immediately while the GUI is hidden
  3. server-side FSM replication gate — the extra StateChanged→BroadcastEntityCommand(...) hook in Core/Factory/init.luau is wrapped in if RunService:IsServer() and Utility.Settings.ServiceManager.Enabled then ... end

Note

Settings.ServiceManager.Enabled = false does not currently prevent a direct manual StartServiceManager() call. It only disables code paths that explicitly consult that setting.


πŸš€ Real examples

Example 1: Launch the dashboard and jump to NETWORK

local FSM = require(game:GetService("ReplicatedStorage").RBXStateMachine)
FSM.Orchestrator:RegisterComponents()

local sm = FSM.Orchestrator:StartServiceManager()
sm.Show()
sm.SwitchSubsystem("NETWORK")
sm.SetContextMode("server")

Example 2: Inspect the server snapshot directly

-- client
local snapshot = FSM.Orchestrator:ServerRequest("ServiceManager_ServerSnapshot")
if snapshot then
    print(snapshot.Timestamp)
    print(snapshot.Network and snapshot.Network.Remotes and snapshot.Network.Remotes.EntityUpdateEvent)
end

Example 3: Drive the UI from Studio command scripts

local rs = game:GetService("ReplicatedStorage")
rs:SetAttribute("ServiceManagerCmd", "context:server")
rs:SetAttribute("ServiceManagerCmd", "subsystem:FSM")
rs:SetAttribute("ServiceManagerCmd", "fsmViz:EnemyAI_42")
rs:SetAttribute("ServiceManagerCmd", "step:3")

⚠️ Edge cases and pitfalls

  1. Initialize() is idempotent, not re-entrant. If you need a fresh instance, call Destroy() first.

  2. Unknown subsystem names are silently ignored. SwitchSubsystem("foo") does nothing.

  3. Invalid context modes are silently ignored. Only "client" and "server" are accepted.

  4. Server snapshot registration is intentionally one-shot. A second RegisterServerHandlers() call only logs a warning.

  5. Completed FSMs are shadowed, not live. Drill-downs in server mode may show preserved historical snapshots, not active runtime objects.

  6. Logs are capped. Local _logs keeps 500 entries; server snapshots trim aggregated Logs to 200 entries.

  7. The dashboard is a dev tool, not gameplay UI. It assumes studio/debug-style access to replicated internals and telemetry.


πŸ”— See also

🏠 Home


πŸš€ Getting Started


πŸ—οΈ Architecture


πŸ”§ API Reference


πŸ“š Guides


βš™οΈ Advanced


πŸ› οΈ Development


πŸ“– Reference

Clone this wiki locally