Skip to content

API Index

Every public member of every module, verified against the framework source. Conventions used throughout: Kernel.Priority = { Kernel=1, High=2, Normal=3, Low=4, Background=5 } (lower runs first). Bus handlers receive (topic, ...). Hook handlers receive a mutable context and cancel by returning false. Channel schemas are arrays of Packet type-name strings ("NumberU16", "Vector3F24", …).

This page exists for search — Ctrl+K a member name and land on its exact signature. Each section links to the module’s deep page for mental models, examples, and gotchas.

The shared microkernel — boots once per VM, owns the scheduler, hooks, bus, and services. Lives at ReplicatedStorage.ChloeKernel. Deep page: Kernel & boot.

Member Description
Kernel.boot(config?) → kernel Once per VM. Server budgets are frame-aware by default (16.5ms frame target, 1.5ms floor); config.BudgetSeconds switches to a fixed slice, config.TargetFrameSeconds tunes the frame-aware target
Kernel.current() → kernel The booted instance
kernel:shutdown() Full teardown (schedulers, bus, services, diagnostics) and clears the boot singleton — a fresh boot() works after it
kernel.Scheduler / .Hooks / .Bus / .Services Core subsystems
kernel:registerService(def) def = { Name, Dependencies?, init(self, kernel)?, start(self)?, stop(self)? }
kernel:start() Two-phase boot of registered services
kernel:getService(name) After boot
kernel:spawnProcess(fn, {Name?, Priority?}?) → Process
kernel:phaseScheduler("PreSimulation" / "PostSimulation" / "PreRender") → Scheduler Physics/camera-phase work; PreRender is client-only
kernel:stats() → snapshot Scheduler health + service timings
kernel:enableDiagnostics(intervalSeconds?, extend?) Diag attributes + Kernel.Overload bus alerts; extend(snapshot) appends custom fields
kernel:api(extensions?) → frozen table Restricted userspace surface

Everything Kernel has, plus sessions, networking, and everything privileged. Lives at ServerScriptService.ChloeKernelServer. Deep pages: Kernel & boot, Sessions.

Member Description
kernel:onSession(fn) → connection Fires for current AND future sessions
kernel:getSession(player) → Session?
kernel:net() → NetDriver Lazy singleton
kernel:stats() Adds Sessions count and Net counters
kernel:securityAudit({Silent?}?) → {AuditFinding} One pre-ship sweep: unguarded channels, Open escape hatches, fail-open gate hooks, defaulted rate limits — prints a summary (unless Silent) and returns {Severity, Kind, Name, Detail} findings, most severe first

Session: .Player, .Data (per-visit scratch), .Profile (persistent; set by DataDriver:attach), .Active, .OnEnd; session:bind(x) auto-tears-down connections, instances, functions, and disconnectables. See Sessions.

Priority-scheduled work under a per-frame budget. Deep page: Scheduler.

Member Description
scheduler:schedule(fn, priority?, ...) → handle One-shot next step; handle:cancel(); extra args pass through to fn
scheduler:every(seconds, fn, priority?) → handle Recurring, drift-free
scheduler:step(budget?) Manual drive (tests)
scheduler.OnError Signal; task crashes are isolated and land here
scheduler.Stats LastStepSeconds, TasksRun, TasksDeferred plus live budget, heartbeat-phase busy time, and physics time per step
scheduler:topTasks(n) / :resetProfile() Hot-task profiler readout (what the panel shows)
scheduler:queueDepths() → (depths, total) Per-priority queue depths
scheduler:takeStepWindow() → (avg, max, deferred, steps) Drains the windowed step stats (diagnostics use this)
Scheduler.setTaskOrigin(fn, label) Name a function in the profiler instead of its script:line (Process does this per process)
Scheduler.setMicroProfilerZones(enabled) / .getMicroProfilerZones() MicroProfiler zones — Studio default on, live servers opt in

Work scheduled during a step runs next step. Kernel priority ignores the budget.

A coroutine with a lifecycle (PID, state machine, exit signal) stepped by the scheduler. Deep page: Processes.

Member Description
Process.new(fn, {Scheduler, Name?, Priority?}) Direct construction; kernel:spawnProcess is the usual entry
proc:start(...) Args pass to fn
proc:suspend() / proc:resume() Stops stepping, state kept / resumes
proc:kill(reason?) Closes the coroutine, fires OnExit
Process.yield() Inside a process: resumes on a later scheduler step, never blocks the frame
Process.get(pid) → Process? PID registry (weak-valued)
.Pid, .State, .Result, .OnExit(state, result) State is Created / Running / Suspended / Completed / Crashed / Killed; crashes carry a traceback in .Result

The IPC primitives: signals for point-to-point events, the bus for topic pub/sub, hooks for fail-closed validation chains. Deep pages: Signals & Bus, Hooks.

Member Description
Signal.new() · :connect(fn) → conn · :once · :wait · :fire(...) · :destroy() Thread-reusing, O(1) disconnect; destroy resumes wait-parked threads
Bus.new() · :subscribe(topic, fn) → conn · :publish(topic, ...) "Family.*" wildcards
Bus:publishRemote(topic, ...) Explicitly forwards one publish across the NetBridge (BusBridge)
Bus:setNetBridge(bridge?) · Bus:destroy() Bridge injection (BusBridge calls this) and teardown
HookRegistry.new({WarnOnError?}) · :definePoint(name, {FailOpen?}) Points default fail-closed
hooks:on(name, fn, priority?) → disconnectFn Default priority 100; ties keep registration order
hooks:fire(name, context?) → (passed, context) Fires over a snapshot — mid-fire unregistration can’t skip handlers
hooks:listPoints() → { {Name, FailOpen, Handlers} } Point snapshot for audits/debug tooling

Scoped, leveled logging with crash-isolated sinks. Deep page: Logger.

Member Description
Logger.scope(name) → { debug, info, warn, error } Scoped logger functions
Logger.Level { Debug=1, Info=2, Warn=3, Error=4 }
Logger.setLevel(Logger.Level.X) Entries below the level are dropped
Logger.addSink(fn) → removeFn Crash-isolated; sink receives the entry table
Logger.setConsoleEnabled(bool) / Logger.isConsoleEnabled() Console sink toggle

GC statistics from sampling gcinfo() on a Background task — a counter read, effectively free. Deep page: GcWatch & ErrorWatch.

Member Description
GcWatch.attach(kernel, {SampleSeconds?, WindowSeconds?}?) → watch Default 0.1s samples, 5s rate windows
watch:stats() → { HeapKb, PeakHeapKb, AllocKbPerSec, CyclesPerMin, Cycles, LastReclaimKb, AvgReclaimKb, ReclaimedKb } Heap climbing + reclaims ~0 = leak; heap stable + busy cycles = churn
watch:destroy() Cancels the sampler

kernel:enableDiagnostics() attaches one automatically and publishes ChloeKernelDiag{Role}GcAllocKbS / GcCyclesMin / GcReclaimKb attributes.

Buffer-packed serialization at every boundary — schema codecs, adaptive encoding, struct deltas. Deep page: Serde & Base64.

Member Description
Serde.schema(spec, {Version?, Extras?}?) → codec Spec: type-name strings, {elementSpec} lists, nested dicts, Serde.optional(spec)
codec:encode(value) → buffer / codec:decode(buffer) Version byte checked when configured
Serde.encode(value) / Serde.decode(buffer) Adaptive: numbers auto-packed, 1 type byte per value
Serde.infer(sample) → spec Typed spec from a plain table — safe wide widths, never narrowed from the sample’s magnitude
Serde.struct(spec) → struct encodeFull/decodeFull/encodeDelta/decodeDelta — powers replication
Serde.optional(spec) 1 byte when absent
Base64.encode(buffer) → string / Base64.decode(string) → buffer RFC 4648, buffer-native

Type menu: NumberU8/16/24/32, NumberS8/16/24/32, NumberF16/24/32/64, NumberVlq (variable-length: 1 byte under 128, 2 under 16384, up to 2^53), String(Long), Buffer(Long), Boolean8, Vector2/3 (S16/F24/F32), CFrameF24U8CFrameF32U16, Color3, BrickColor, EnumItem, sequences, Characters, Any. Storage codecs reject Instance.

Notes: "Any" stores whatever the field holds at 1 type byte per value; Extras = true preserves un-schema’d fields adaptively (the default drops them); schema and struct codecs compile to a fast engine (25-40% faster, byte-identical output) and fall back automatically for uncovered types (Any, CFrames, sequences, Extras).

Fail-closed channels: intents (client → server actions), states (server → client pushes), requests (client questions), unreliable states. Deep pages: NetDriver, NetClient, Registry, BusBridge, MessageDriver, Rate limiting.

Member Description
Net:defineIntent(name, schema, {RateLimit?=30, Burst?, Handler?, Open?}?) Handler + no validator + not Open → fail-closed (rejects)
Net:onIntent(name, fn(session, ...))
Net:defineState(name, schema) · :sendState(name, player, ...) · :broadcastState(name, ...)
Net:defineRequest(name, schema, responseSchema, {Handler, RejectValue?, RateLimit?, Open?}?) · Net:onRequest(name, fn(session, ...)) Same fail-closed rule; unguarded → RejectValue
Net:definePredictedIntent(name, schema, opts) Pairs with Prediction.wrap (Prediction)
Net:auditValidators() → { {Name, Kind} } Handler-bearing channels with no validator and no Open (i.e. currently rejecting)
Net:defineUnreliableState(name, structSchema) · :sendUnreliableState(name, player, data) · :broadcastUnreliableState(name, data) Lost > late for high-frequency cosmetic data; ≤900 bytes
Net.Stats IntentsAccepted/Rejected/RateLimited, RequestsRejected, TransportFloods/Bytes
Registry.define(defs) · Registry.server(kernel, defs) → handles · Registry.client(defs, netClient?) → handles Single-source channels; Validate rules (Range/OneOf/MaxLength) compile into hook chains at priority 5; Open = true per def for no-auth channels
BusBridge.attach(kernel, {ServerTopics?, ClientTopics?, ClientSchemas?, RateLimit?=20}?) · BusBridgeClient.attach(kernel, netClient?, {ClientSchemas?}?) Bus topics across the network; upward publishes are whitelisted, typed, and rate-limited
MessageDriver.new({Codec?, BackoffSeconds?}?) · :publish(topic, data) → (ok, err?) · :subscribe(topic, fn(data, sentAt?)) Cross-server events over MessagingService, Serde-packed, 1KB-guarded
NetClient.new() · :intent(name, schema) → {fire} · :onState(name, schema, fn) · :request(name, inSchema, outSchema, {RejectValue?}?) → {invoke} A timed-out invoke resumes with RejectValue, same as a rejection
NetClient:onUnreliableState(name, structSchema, fn(data))

Hook points: Intent.<name>, Request.<name> (fail-closed); Net.RateLimited, Net.FloodDetected (fail-open). Bus topics: Intent.<name>, Net.IntentRejected, Net.RateLimited, Net.FloodDetected. Full catalog: Hook Points & Bus Topics.

Server-owned data clients watch — snapshot for late joiners, field deltas for changes. Deep page: Replica.

Member Description
ReplicaService.new(kernel)
service:create(name, {Schema, Data, TickRate?=10, Interest?, Quantize?}) → replica Quantize = {[field] = threshold} deadbands numeric/vector fields against the last replicated value
replica:set(key, value) · :patch(table) · :get(key) · :destroy() Dirty fields coalesce per tick
replica:bindZone(zones, zoneName) → unbind Zone-scoped interest: Zone.Entered/Left flips subscription instantly (snapshot on entry, remove on exit); the interest scan agrees via isInside
ReplicaClient.new() · :listen(name, schema, fn(replica)) Early packets buffered until listen (capped at 64 pending)
ReplicaClient:unlisten(name) · :destroy() Teardown
client replica: .Data, .OnChange(key, new, old), .OnRemove

Rebindable actions over UserInputService, published on the client bus. Deep page: InputDriver.

Member Description
InputDriver.attach(kernel) Canonical constructor (.new remains as a deprecated alias)
input:bindAction(name, {Keyboard?, Gamepad?, TouchButton?, Handler?}) Keyboard/Gamepad take one Enum.KeyCode or an array of them
input:rebind(name, "Keyboard" / "Gamepad", keys?)
input:unbindAction(name) · :getBindings() · :destroy() Snapshots mirror the binding shape (name or array of names)

Emits Input.<Action> on the client bus.

MoveKit (movement-context abilities; deep page MoveKit): MoveKit.attach(kernel, {Net?, Clock?, Humanoid?, Root?, SkipCharacterTracking?}?) (canonical; .new deprecated alias) — :define(name, {Input?, When?="Airborne", Charges?=1, Cooldown?=0, MinAirTime?=0.1, Announce?=true, Run}) · :trigger(name) → bool · :destroy() · MoveKit.jumpVelocity(humanoid) · recipes MoveKit.recipes.{doubleJump({Power?, Charges?, Cooldown?}?), airStep({Steps?=3, Boost?=0.75, Cooldown?, OnStep?}?)}. Server: MoveKit.server(kernel, {Abilities = {[name] = {Charges?, Cooldown?, Forgive?}}, IsGrounded?, Clock?, CooldownSlack?=0.05}) — fail-closed CKMove intent metering with server-side ground truth; Input = "Jump" rides UserInputService.JumpRequest, other strings ride Input.<Action> bus events. Bus: client Move.Triggered(name, context), server Move.Used(session, name, position) / Move.Rejected(player, name, reason).

Loss-hardened persistence: session locks, migrations, codecs, backups, fail-safe loads. Deep pages: DataDriver, Backends.

DataDriver.new({
Name, Defaults,
Backend? = "DataStore" | "Memory" | "Http" | instance,
BackendConfig?, -- { Scope? } or { BaseUrl, Headers } for Http
Backup? = {
Backend? = "DataStore" | "Memory" | "Http" | instance, -- default: DataStore "{Name}_Backups"
BackendConfig?,
IntervalSeconds? = 86400, -- once a day per key, exactly once game-wide
Keep? = 3, -- rotation slots per key
Mirror? = false, -- ALSO copy every save to a "live" slot
Fallback? = true, -- restore newest backup when the record is corrupt
},
Codec?, -- Serde schema codec, or "Auto" (inferred from Defaults + extras)
SchemaVersion?, Migrations?, -- { [n] = function(data) -> data }
Validate?, -- gates every save
AutosaveSeconds? = 60, LockTtlSeconds? = 180,
BackoffSeconds? = {1,2,4,8}, KickOnLoadFailure? = true,
})
Member Description
driver:attach(kernel) Full lifecycle: load on join into session.Profile, save on leave, autosave, flush on shutdown
driver:load(key) → (Profile?, err?) Manual lifecycle (clan banks, shared stashes)
driver:saveAll() · :releaseAll()
driver:backupNow(profile) Forces a backup copy
driver:listBackups(key) Slots + ages
driver:peekBackup(key, slot?) Reads a backup without locking anything — safe while the player is online elsewhere
Profile: .Data, .Meta, .Key, .Active, .RestoredFromBackup, :save() → (ok, err?), :release() release frees the session lock for other servers

Constraint (asserted at construction): LockTtlSeconds >= 2x AutosaveSeconds — the autosave is the lock heartbeat.

Cross-server state on MemoryStores with automatic buffer packing. Deep page: MemoryDriver.

Member Description
MemoryDriver.new({Name, Kind? = "HashMap" / "SortedMap", Codec?, DefaultTtlSeconds?=300})
driver:set(key, value, ttl?)
driver:get(key) → (ok, value, err?)
driver:update(key, transform, ttl?) Return nil from transform to abort the write
driver:remove(key)
driver:getRange(direction?, count?) SortedMap only — leaderboard reads

Exactly-once receipts and atomic two-profile trades. Deep pages: Receipts, Transactions.

Member Description
Receipts.attach(kernel, {GetProfile, GetPlayer?, LedgerSize?=200, Bind?=true}) Binds ProcessReceipt; only a landed save acknowledges
receipts:onProduct(productId, grantFn(session, receiptInfo))
Transactions.atomic(profileA, profileB, mutate, {Id?}?) → {Ok, Reason?} Per-profile locks + replay ids. Reasons: Aborted, MutatorError, SaveAFailed, SaveBFailed, ProfileInactive, SameProfile, Busy, DuplicateTransaction

Movement monitoring and lag-compensated hit validation. Deep pages: Movement monitor, Rewind.

Member Description
Movement.attach(kernel, {IntervalSeconds?=0.2, SpeedTolerance?=1.4, TeleportStuds?=80, StrikeDecaySeconds?=10, RubberbandStrikes?=2}?)
monitor:forgive(player) / Bus AntiExploit.Forgive Announce server-initiated moves
Hook AntiExploit.MovementViolation (fail-open) {Player, Session, Type, HorizontalSpeed, Displacement, Strikes}
Rewind.attach(kernel, {SampleHz?=20, HistorySeconds?=1, OriginToleranceStuds?=8, HitToleranceStuds?=6, MaxFutureSeconds?=0.2, ShowHitboxes?, Clock?}?) Clock defaults to workspace.GetServerTimeNow
lag:getPositionAt(player, timestamp) → Vector3?
lag:showHitboxes(enabled) Dev visualization: every castRay draws the rewound capsules it tested (green = hit, red = miss), replicated to all clients; visual only
lag:validateShot(claim) → (ok, reason?) Reasons: FutureTimestamp, StaleTimestamp, NoShooterHistory, OriginMismatch, OutOfRange, NoTargetHistory, HitMismatch
lag:castRay(shooter, origin, direction, range, timestamp, hitbox?) → (player?, position?, distance?) Lag-compensated hit detection: casts against every tracked player’s REWOUND capsule hitbox ({Radius?=1.6, HalfHeight?=2.4}); world obstruction is the caller’s job
Rewind.rayCapsule(origin, unitDir, range, center, radius, halfHeight) → distance? Pure capsule intersection, unit-testable

Cross-server infrastructure: queue-to-teleport matchmaking, update migration, parallel Luau workers. Deep pages: Matchmaking, SoftShutdown, ActorPool.

Member Description
Matchmaking.new(kernel, queueName, {TeamSize?=2, PlaceId?}?) · :enqueue(player) · :dequeue(player) Bus: Matchmaking.Queued/Dequeued/MatchFound
SoftShutdown.attach(kernel) Reserved-transit migration; no-op in Studio
ActorPool.new({Size, WorkerModule, Parent?, TimeoutSeconds?=10}) · pool:dispatch(...) → (ok, result / err) (yields) · :destroy() WorkerModule returns a pure function(...) → result

Genre building blocks — each is a server module wired into the fail-closed pipeline. Overview: Kits.

The item model: definition registry, slotted bags, stacking, equip slots, use handlers. Deep page: InventoryKit.

Member Description
InventoryKit.attach(kernel, {Items, MaxSlots?=30, Persist?=true, Field?="Inventory"}) Items: {Name?, Stack?=1, Kind?, Equip?, OnUse?, OnEquip?, OnUnequip?, Meta?}
inv:grant(session, id, count?) → granted Server-API-only — no client message mints items
inv:take(session, id, count?) → bool All-or-nothing
inv:count(session, id) · :items(session)
inv:move(session, from, to) Drag-and-drop primitive: swap or merge, equip references follow
inv:equip(session, slotIndex) · :unequip(session, slotName) · :equipped(session, slotName) → (item?, index?) Equipping displaces the incumbent through its OnUnequip
inv:use(session, slotIndex) OnUse returning true consumes one; draining a stack auto-unequips

Client intents INV_Move/Equip/Unequip/Use (fail-closed); owner state push INV_Sync; hooks Inventory.CanEquip/CanUse (fail-open); Bus Inventory.Granted/Taken/Equipped/Unequipped/Used/Changed.

Weighted loot tables with persisted pity timers. Deep page: LootKit.

Member Description
LootKit.attach(kernel, {Tables, Inventory?, Seed?, Rng?}) Entries: {Weight, Id?, Count?/{min,max}, Table?, Nothing?, PityAfter?}; tables: {Rolls?/{min,max}, Entries}; nesting caps at 8; unknown references fail at attach
loot:roll(table) → items No player, never advances pity (display, previews)
loot:rollFor(session, table) → items Pity persists in profile Data.LootPity
loot:award(session, table) → (granted, overflow) Rolls + grants through InventoryKit

Bus Loot.Rolled/Awarded.

Recipes over InventoryKit — atomic input checks, timed crafts, station gating. Deep page: CraftingKit.

Member Description
CraftingKit.attach(kernel, {Recipes, Inventory, TickSeconds?=0.25, Clock?, Intents?=true, SkipLoop?}) Recipes: {Inputs, Output, Seconds?=0, Station?, OnComplete?}
crafting:craft(session, recipeId, station?) → (ok, reason?) One active craft per session
crafting:canCraft(...) Reasons: MissingInputs, Busy, WrongStation, Vetoed
crafting:cancel(session) Refunds inputs
crafting:activeCraft(session) · :destroy()

Intents CR_Craft/CR_Cancel; hook Craft.CanCraft (fail-open); Bus Craft.Started/Completed/Cancelled/Overflow.

Soft-currency shops over InventoryKit (Commerce owns Robux; this owns coins). Deep page: ShopKit.

Member Description
ShopKit.attach(kernel, {Shops, Inventory, Currency?, CurrencyField?="Coins", Clock?, Intents?=true}) Shops: {Listings? = {[id] = {Price?, SellPrice?, Stock?}}, Rotation? = {Every, Size, Pool, Seed?, Listing?}}; Currency adapter {Get, Take, Give}
shop:buy(session, shop, item, count?) → (ok, reason?) Grants first, charges for what fit
shop:sell(...) · :listings(shop) Rotation is deterministic per window across servers

Intents SH_Buy/SH_Sell (count capped at 99); Bus Shop.Bought/Sold/Rejected.

Owned NPC followers built from NPCKit archetypes. Deep page: CompanionKit.

Member Description
CompanionKit.attach(kernel, {Npcs, Effects?, Companions, TickSeconds?=0.25, Intents?=true, SkipLoop?}) Companions: {Archetype, FollowDistance?=8, TeleportDistance?=60, Aura?, OnSummon?, OnDismiss?}; one per session
companions:summon(session, id) → npc? A new summon replaces the old
companions:dismiss(session) · :companionOf(session) → (npc?, id?) · :destroy() Aura holds an Effects buff on the owner while summoned

Intents CP_Summon/CP_Dismiss; hook Companion.CanSummon (fail-open); Bus Companion.Summoned/Dismissed/Died.

Same-server player parties with invites, leaders, and lifecycle cleanup. Deep page: PartyKit.

Member Description
PartyKit.attach(kernel, {MaxSize?=4, InviteTtlSeconds?=60, TickSeconds?=5, Clock?, Intents?=true, SkipLoop?}?)
party:invite(from, target) → (ok, reason?) · :accept(player, from) → (ok, reason?) · :decline(player, from) Accepting leaves the current party; the party forms on first accept
party:leave(player) · :kick(leader, member) · :promote(leader, member) Leader inheritance on leave (longest-standing member)
party:partyOf(player) → party? · :sameParty(a, b) · :members(player) → {Player} · :destroy() members is the group-queue feed for Matchmaking

Intents PT_Invite/Accept/Decline/Leave/Kick/Promote (fail-closed, UserIds as F64); state pushes PT_Sync (roster) and PT_Invited; hook Party.CanInvite (fail-open); Bus Party.Created/Disbanded/Joined/Left/Kicked/Invited/Promoted.

Teams wired into the combat gates, with spawn ownership and Roblox Teams mirroring. Deep page: TeamKit.

Member Description
TeamKit.attach(kernel, {Teams = {[name] = {Color?, Capacity?}}, AutoAssign?=true, FriendlyFire?=false, SpawnTag?="TeamSpawn", UseSpawns?, SyncRoblox?, Seed?}) Tagged TeamSpawn parts with a Team attribute own spawns
teams:assign(player, team) → bool
teams:teamOf(subject) → name? Player/session/character/npc handle (via Faction)
teams:sameTeam(a, b) · :players(team) · :emptiest() · :destroy()

Vetoes Weapon.CanDamage + Melee.CanHit unless FriendlyFire; Bus Team.Assigned(player, team, previous?).

Server-authoritative hand-to-hand: windup/hit-frame/recovery timelines, blocks, guard-breaks, parries. Deep page: MeleeKit.

Member Description
MeleeKit.attach(kernel, {Moveset, BlockScale?=0.25, ParryWindow?=0.18, ParryCooldown?=0.6, StaggerSeconds?=1.1, GuardBreakStaggerScale?=1.5, LatencySlack?=0.075, TickSeconds?=0.05, Intents?=true, Clock?, GetPosition?, GetFacing?, SkipLoop?}) Attacks: {Damage, Range?=7, Arc?=120, Windup?=0.2, Recovery?=0.3, ComboNext?, GuardBreak?, OnHit?}
melee:register(entity, {Humanoid?}?) · :unregister(entity) Combatants are entity-agnostic: sessions, npc handles, models
melee:attack(entity, name) → bool · :block(entity, down) → bool · :parry(entity) → bool Parry attempts consume ParryCooldown hit or miss
melee:canAttack(entity, name) · :state(entity) · :destroy()

Player intents ML_Attack/ML_Block/ML_Parry auto-wire fail-closed; npc victims with .defend roll their skill-scaled reaction as the parry. Hook Melee.CanHit (fail-open); Bus Melee.Hit/Blocked/GuardBroken/Parried/Staggered/Combo/State.

WeaponKit / SpellKit / CheckpointKit / ProceduralKit

Section titled “WeaponKit / SpellKit / CheckpointKit / ProceduralKit”

Service-definition kits — each returns a service for kernel:registerService. Deep pages: WeaponKit, SpellKit, CheckpointKit, ProceduralKit.

Member Description
WeaponKit.service({Weapons, Rewind?, Ammo?}) Per-weapon Hitbox = {Radius?, HalfHeight?} sizes the rewound capsule (defaults 1.6/2.4); Rewind makes claims validate AND hits detect against rewound positions; Ammo adapter {Get, Take} persists via profile. Hook Weapon.CanDamage (fail-open); Bus Weapon.Fired/Hit/ShotRejected
SpellKit.service({Spells, MaxMana?=100, ManaRegenPerSecond?=5}) Per-spell ManaCost (deducted only when validation passes), Cooldown (server-side per session), ChannelSeconds (cast runs as a killable Process; damage interrupts). Bus Spell.Cast/Interrupted
CheckpointKit.service({FolderName?="Checkpoints", MinimumLegitSeconds?=3, StageField?="BestStage"}?) Pads named “1”, “2”, “3”… under the folder; MinimumLegitSeconds rejects stage times no honest run can hit. Bus Obby.Checkpoint
ProceduralKit.service({Archetypes, RegenSecondsPerModel?=0.25}) spawn(name, {CFrame?, Size?, Params?, Parent?, Owner?}) → handle (handle:set/patch/resize/regenerate/waitForGeneration/destroy), validate(name, params) → (ok, cleanedOrReason) for wiring client customization intents. Bus Procedural.Spawned/Updated/Rejected/Removed

Quests as data — objectives count server-published bus topics, so there is no client input to validate. Deep page: QuestKit.

Member Description
QuestKit.service({Quests, Field?="Quests"}) Quest: {Objectives = {{Topic, Count?=1, Match?, Filter?}}, AutoAssign?, Repeatable?, OnComplete?}
quests:assign(session, id) · :abandon
quests:progress(session, id) → {Done, Objectives}?

Bus Quest.Progress/Completed.

Exploit-proof ProximityPrompts from CollectionService tags. Deep page: InteractionKit.

Member Description
InteractionKit.service({Interactions, GetDistance?, HasLineOfSight?}) Interaction: {Tag, ActionText?, ObjectText?, HoldDuration?, MaxDistance?=10, Cooldown?, LineOfSight?, OnInteract?}

Prompts follow CollectionService tags; every trigger re-validates through Intent.Interact (kit gate at priority 50); Bus Interact.Triggered/Rejected.

Server-side npc handles — helpers your game loop composes, not an AI runtime. Deep page: NPCKit.

Member Description
NPCKit.attach(kernel, {Pathfinding?, Grid?, PathMethod?, Projectiles?, Zones?, Container?, Seed?, GetTargets?, Clock?, Sounds?, HasLineOfSight?, Raycast?}) No loop starts: your game schedules its own tick. Sounds = {Occlusion? = "Through"/"Blocked"/"Path", Topics? = {[busTopic] = range}}
kit:define(name, {Model?, Health?, WalkSpeed?, Difficulty?="Medium", Moveset?, Reactions?, DespawnSeconds?=3, Targetable?, Faction?, Squad?, GetPosition?, Move?}) Archetype registry
kit:spawn(name, cframeOrPosition, {Difficulty?, Squad?}?) → npc
kit:all() → {npc} · :count() · :destroy() The roster your loop walks
kit:update() Squad director + due windups — call from your tick
kit:squad(name) → squad
kit:emitSound(position, {Range?=40, Loudness?, Source?}?) Range and accuracy scale by listener difficulty
kit:findCover(npc, threat, {SearchRadius?=40, SpotTag?, BackAway?}?) → Vector3? · :findPeekPoint(spot, threat) → Vector3? Cover scoring: grid cells or tagged nodes with optional Direction attributes
kit:hasLineOfSight(from, to)
Npc handle Description
npc:position() · :facing()
npc:canSee(target, maxRange?, fov?) Cone-gated before any raycast
npc:sweep(direction, arc?=90, rays?=5, range?=24) → Model? Horizontal raycast fan across the unseen angle
npc:aimAt(pos, velocity?, speed?, gravity?, skill?) → direction Full skill model: intercept lead, correlated drift, time-on-target tightening
npc:act(name, target?) → bool Runs a moveset action
npc:defend(context) → bool Rolls skill-scaled reactions (name-sorted, deterministic per seed)
npc:notifyDamage(attacker?) Suppresses aim ~1.5s; unseen attackers drop into hearing memory
npc:lastIntel(target) Squad-blackboard read
npc:updateTarget(acquire, giveUp, {RequireSight?, MemorySeconds?, DetectionSeconds?}?) Acquisition, sight memory, graded detection
npc:moveTowards(goal) · :faceTowards(point) Thin wrapper over Pathfinding.follower / body tracking while stopped
npc:canReact() · :setDifficulty(nameOrTable) · :destroy()
Squad / config Description
squad:report(npc, target, position, velocity?) · :lastKnown(target) · :role(npc) · :add/remove(npc) · .FlankSigns[npc] Shared blackboard; 30s-stale intel expires
Actions {Kind = "Projectile"/"Hitscan"/"Melee"/"Custom", Difficulty?, Cooldown?, DefId?, Speed?, Gravity?, Range?, Damage?, OnHit?, Run?, Magazine?, ReloadSeconds?, WindupSeconds?, FriendlyFire?}
Reactions {Against?, Cooldown?=1, Difficulty?, Run?}
NPCKit.Difficulties.{Perfect, HumanPeak, ReallyGood, Good, Medium, Novice, Bad} {AimErrorDegrees, ReactionSeconds, LeadSkill, CooldownMultiplier, HearingMultiplier, HearingBlurStuds, PathSkill, DefenseSkill, FieldOfViewDegrees}

Bus Npc.Spawned/Died/Action/Heard/Suspicious/Damaged/Reload/Windup/Path/Hitscan/Defense/Tactic.

Pure strategic helper — build one root per agent, tick it from your loop. Deep page: BehaviorTree.

Member Description
Selector(name?, children) Resumes a Running child directly
ReactiveSelector(name?, children) Re-evaluates from the top: higher priorities preempt a Running child
Sequence(name?, children)
Condition(name?, check) · Action(name?, run) nil return from an Action = Success
Invert(child) / Succeed(child) · Cooldown(seconds, child) Decorators
tick(root, blackboard, dt, clock?) → "Success"/"Failure"/"Running" Pass the NPCKit kit clock for deterministic Cooldowns
reset(root) The documented move on target change

Standalone perception helpers — NPCKit’s own ears and eyes delegate here. Deep page: Senses.

Member Description
Senses.hear(listenerPosition, attunement?, {Position, Range?=40, Loudness?}, {Distance?, Rng?}?) → Vector3? Anisotropic blur (direction resolves well, distance poorly); Distance plugs route-measured occlusion in
Senses.inCone(eye, facing?, fovDegrees, target)
Senses.canSee(eye, facing?, fovDegrees, target, hasLineOfSight, maxRange?)
Senses.Attunement.{Keen, Sharp, Average, Dull, Oblivious} {Range, BlurStuds} presets, or any custom table

Server systems below the kit layer. Each row links to its deep page.

Member Description
RoundKit.service({Phases, Replicas?, TickSeconds?=1}) Phase: {Name, Duration?, MinPlayers?}; service :advance(), :current(); Bus Round.PhaseChangedRoundKit
Projectiles.attach(kernel, {Rewind?, TickRate?=30}?) · :define(id, {Speed, Gravity?, MaxLifetime?=3, Hitbox?, OnHit?, OnExpire?}) · :fire(session, id, origin, dir, timestamp?) → (ok, reason?) Server sim; Bus Projectile.Hit. Set Hitbox = {Radius?, HalfHeight?} to resolve player hits via capsule hitboxes (same shape/knobs as WeaponKit, honors the Rewind’s ShowHitboxes display); without it, hits stay raw raycasts against character geometry and OnHit gets the engine RaycastResult — Projectiles
ProjectileClient.new({[id] = {Gravity?, Create, OnImpact?}}, {MaxVisuals?, MaxTracers?=300, ThreatRadius?=15}?) · .threatens(origin, velocity, position, radius) Pooled client visuals, same math as the server; MaxVisuals (defaults from the device profile) caps FULL visuals only — overflow renders as minimal pooled tracers, and shots passing within ThreatRadius of the local character always render full: no projectile is ever invisible to its target — Projectiles
Effects.attach(kernel) · :define(name, {Duration?, MaxStacks?=1, Stats? = {[stat]=multiplier}, Category?, Immune?, TickSeconds?, OnTick?, OnApply?, OnExpire?}) · :apply(session, name, {Duration?}?) → stacks (0 = blocked by an immune ward) · :cleanse(session, category?) → count · :remove · :has · :stacks · :statMultiplier(session, stat) → number Humanoid stats auto-applied; periodic OnTick with bounded catch-up; owner CKEffects state push; Bus Effects.Applied/Expired/Blocked/CleansedEffects
Prediction.wrap(net, name, schema, {Predict?, TimeoutSeconds?=2}) → {fire → seq, OnResolved} Pairs with Net:definePredictedIntent(name, schema, opts)Prediction
Zones.attach(kernel, {IntervalSeconds?=0.25}?) · :add(name, part or {parts}, {OnEnter?, OnLeave?}?) → handle · :addPart(name, part) · :remove(name) · :addTagged(tag, {NameAttribute?="ZoneName"}?) → stop · :track(entity) → untrack · :trackTag(tag) → stop · :untrack(entity) · :playersIn(name) · :entitiesIn(name) · :isInside(name, occupant) Handle carries Entered/Left Signals firing (occupant); Bus Zone.Entered/Left (name, player) for players, Zone.EntityEntered/EntityLeft (name, entity) for tracked entities (leaves before enters). addTagged builds zones from CollectionService tags — same-named parts union into one multi-part zone — Zones
Leaderstats.attach(kernel, {Display = {Field, From?, Kind?}}, opts?) Values track data on a 1s sweep — Leaderboards
Leaderboards.attach(kernel, {Boards = {[name] = {Ascending?, KeepBest?=true, MaxEntries?=100, CacheSeconds?=60}}, FlushSeconds?=15, WritesPerFlush?=30, Backend?, StorePrefix?="CKBoard_", Clock?, SkipLoop?}) · :submit(board, playerOrKey, value) → bool · :top(board, count?) → {{Key, Value, Rank}} · :flushNow() · :destroy() Global top-N on Roblox OrderedDataStores OR custom stores: backends implement declarative submit(board, key, value, keepBest, ascending) (external DBs apply keep-best atomically their way) or DataStore-shaped set(board, key, updater), plus sorted; queued submits (newest per key per window), per-window write budget, cached reads, failed writes retry; Bus Leaderboard.UpdatedLeaderboards
Fuzz.run(kernel, {Session, Channels?, CasesPerChannel?=32, Seed?=1, IncludeRequests?=true, Driver?, Silent?}){Channels, Cases, Passed, Rejected, HandlerErrors = {{Channel, Kind, Args, Error}}} Hostile payloads per schema type through the real hook chain + handler pipeline; validator throws count as rejects; deterministic per seed — Fuzz & audit
Pool.new({Create, InitialSize?}) · :acquire() · :release(instance) · :idleCount() · :destroy() Double release errors — Pool & Preload
ErrorWatch.attach(kernel?, {WindowSeconds?=30}?) · :counts() · :destroy() Bus Kernel.ScriptError(message, trace, source, count)GcWatch & ErrorWatch
LiveConfig.new(kernel, {Defaults, PollSeconds?=30}) · :get(key) · :set(key, value) Bus Config.Changed(key, value); set(key, nil) deletes the override and every server reverts to its default — LiveConfig
Pathfinding.attach(kernel?) · :find({Method?="Direct", Start, Goal, Grid?, MaxExpansions?, Exclude?, AgentParams?}) → (ok, waypointsOrReason) · :distanceField({Grid, Origin, MaxDistance?}) → {[cellKey] = studs}? · Pathfinding.follower({Paths, Grid, Method?="ThetaStar", RepathDistance?, ArriveDistance?, Clock?}) → follower · follower:step(position, goal) → (nextPoint, {Path?, Jump, Stuck}) · follower:reset() · Pathfinding.grid({Origin, Width, Height, CellSize?=4, AgentHeight?=6, MaxStepHeight?=4, IsWalkable?}) → grid · grid:refresh() · grid:refreshRegion(minWorld, maxWorld) · grid:addLink(fromWorld, toWorld, cost?) · grid:groundY(x, z) · grid:nearestWalkable(position, maxCells?=8) → Vector3? · grid:lineOfSight(x0, z0, x1, z1) (feet) · grid:sightLine(x0, z0, x1, z1) (eyes) Methods AStar/ThetaStar/Direct/Roblox, lazily required; failures return reasons (GoalBlocked, NoPath, BudgetExhausted, …); Roblox yields (the follower runs it off-thread). Default rasterizer raycasts ground (Terrain + CanCollide parts), waypoints ride elevation, MaxStepHeight refuses cliffs per step AND along straight lines; walkable high ground occludes sight, valleys only block feet. refresh bumps grid.Version so followers re-path — Pathfinding
Settings.attach(kernel, {Schema, Field?="Settings", RateLimit?=10}) · :get(session, key) · :all(session) · :set(session, key, value) · rule: {Kind = "number"/"boolean"/"string"/"strings", Default, Min?, Max?, MaxLength?=64, MaxItems?=8} Allowlisted client writes: clamp/cap/rebuild through Intent.Settings_Set; persists via profile; Bus Settings.Changed. Client: SettingsClient.new(net) · :fetch() · :get · :set · :onChanged(fn)Player settings
Analytics.attach(kernel, {FlushSeconds?=10, MaxPerMinute?=120, Sample?, Destination?, WatchErrors?, Seed?}?) · :track(name, player?, value?, fields?) → bool · stats via .Stats · :destroy() (flushes) Batched to AnalyticsService:LogCustomEvent or an injected destination; sampled/over-cap drops counted, never silent — Analytics
BonePhysics.attach({Gravity?, Wind?, FixedHz?=60, MaxSubsteps?=3, MaxDistance?=100, MaxChains?=20, ShouldSimulate?}?) · :bind(part, {Damping?=0.92, Stiffness?=0.1, GravityScale?, WindScale?}?) → handle? · :bindParts({parts}, settings?) · :bindCharacter(model, settings?) → {handles} · :step(dt) · handle:destroy() · :destroy() Client-side verlet bone chains (shared module, ReplicatedStorage.ChloeKernel.BonePhysics); fixed timestep, culling with pose snap-back, chain budget (past MaxChains the nearest to camera win), teleport guard, Transform-slot writes, Destroying auto-unbind; unconfigured MaxDistance/MaxChains default from the device profile on clients — BonePhysics
HairKit.plan(vertices, {Root, Axis?=(0,-1,0), Sectors?=3, Segments?=3, MaxInfluences?=2}) → {Bones, Weights} · HairKit.rig(meshPart, {Sectors?, Segments?, MaxInfluences?, Axis?, Root?}?) → (ok, boneNamesOrReason) · HairKit.supported() · HairKit.attach(kernel, {BonePhysics?, Characters?="All", Rig?, Match?, Settings?, OnRigged?}?) (client bulk manager) · HairKit.server(kernel, {Rig?, Match?}?) (server rig: baked bones replicate once) Skeleton plan + EditableMesh bone injection + DataModel registration + CreateMeshPartAsync bake + in-place ApplyMesh + Bone instance skeleton (name-linked); weights cap at MaxInfluences (max 4); needs the “Allow Mesh & Image APIs” experience setting (supported() probes it) and owned meshes (third-party catalog assets return (false, reason)); Bus Hair.Rigged(character, meshPart, boneNames)HairKit
Dissolve.new({MaxPoints?, Parent?}?) → sim · sim:play(target, {DotSize?=0.25, MaxPoints?, Duration?, ScatterSpeed?=14, NoiseScale?=0.08, Drift?=(0,6,0), Reverse?, Spin?, ReturnSpeed?=8, Chunks?, CustomDot?, OnDone?}?) → controller · sim:morph(source, destination, {Duration?=1.5, Turbulence?=4, Stagger?=0.35, DotSize?, MaxPoints?, NoiseScale?, Spin?, Chunks?, CustomDot?, Reveal?=true, OnDone?}?) → controller · controller:Stop() · controller:setProgress(0..1) when Duration is nil · sim:destroy() · pure Dissolve.noiseVelocity/snap/surfacePoints/gridPoints/sample/pairPoints/morphAlpha Vertex-sampled voxels, per-cell dedup, Perlin advection, restructure, morphs with bottom-up pairing and color lerp; Chunks clones source parts as full-detail flying pieces; pooled dots via BulkMoveTo, one Heartbeat stepper; unconfigured MaxPoints scales 500..5000 by DeviceBench quality; local-only VFX — Dissolve
Ragdoll.attach(kernel, {MaxActive?=16, AutoDeath?, CollisionGroup?, FrictionTorque?=60}?) · :enable(model, {Duration?, Impulse?}?) → bool · :release(model) · :isRagdolled(model) · :destroy() — clients: RagdollClient.listen(netClient) Build-once/toggle: Motor6D→limited BallSocket swap or AnimationConstraint cut with native-socket friction/limit tuning, NoCollision limb pairs, full restore; owner-client CKRagdoll push flips humanoid state, silences Animate, applies impulses, uprights on release; Bus Ragdoll.Started/EndedRagdoll
AudioKit.attach(kernel, {Buses?, MaxChannels?=32, Parent?, Npcs?, Occlusion = {GetListener?, IsBlocked?, Pathfinding?, Grid?}?}?) · :register/registerBank · :play(name, opts?) → handle · :playAt(name, target, opts?) · :playMusic · :playLayers → {setWeights, resync, stop} · :speak · :duck(bus, scale) → release · :setBusVolume · :bindSettings(prefs, map) · :reverbZone(part, type) · :soundscape · :assetIds() · :destroy(); AudioKit.server(kernel) → {broadcast, playFor, broadcastAt} · audio:listen(netClient) Bank config: {Id, Bus?, Volume?, Speed?, Looped?, Priority?, Pooled?, RollOff*, Cues?, Subtitle?, Duck?}; handle: stop(fade?)/setVolume/setSpeed. Bus events Audio.Cue/Subtitle/SubtitleEndedAudioKit
AnimKit.attach(kernel, {LoadTrack?}?) · :register/registerBank ({Id, Priority?, Speed?, FadeIn?, Looped?, Group?, Markers?}) · :attachRig(model) → rig · :assetIds() · :destroy(); rig: :play(name, {Fade?, Speed?, Weight?}) · :stop/stopAll/setSpeed · :ik(name, {Type = "LookAt"/"Reach"/"Transform"/"Rotation", Target, ChainRoot?, EndEffector?, Weight?, FadeIn?, Properties?}) → {Control, setWeight, setTarget, release} · :destroy() Exclusive Groups crossfade; markers publish Anim.Marker; IK weights blend on the kit stepper; rigs die with their model — AnimKit
Preload.run(kernel, {Assets?, Banks?, BatchSize?=16}?) → {Done, Loaded, Total, Failed, await()} Batched PreloadAsync; Bus Preload.Started/Progress/Done; failures collect, never abort — Pool & Preload
DeviceBench.run({BudgetMs?=90, Frames?=0, Force?, Clock?, Bus?}?) → {Score, Quality, Axes{Compute, Churn, Resume}, Tier, ComputePerSecond, ChurnPerSecond, ResumePerSecond, Platform, FrameMs?} · .profile(resultOrQualityOrTier?) → Profile · .quality(result?) → number · .axes(measures) → Axes · .scale(min, max, result?) · .pick(byTier, result?) · .tier(result?) · .score(measures) → (score, tier) · .governor({TargetFps?=60, Result?, Manual?, Clock?, Bus?, OnChange?}?) → {Quality, Tier, sample(dt), profile(), stop()} Time-boxed device benchmark (compute + churn + resumes); Quality and each axis are continuous 0.25..4 multipliers vs a mid-range reference (tiers Low/Medium/High/Ultra remain coarse conveniences); Profile budgets {ViewDistance, VfxDensity, ParticleBudget, ShadowsEnabled, PostFx, BoneChains, BoneDistance, AudioChannels, ProjectileVisuals} are funded by the axis that pays each cost and consumed by unconfigured BonePhysics MaxDistance/MaxChains, AudioKit MaxChannels, ProjectileClient MaxVisuals; scale is log2-mapped; the governor holds TargetFps by nudging effective quality ×0.8 after ~3s of p95 misses / ×1.15 after ~10s of headroom, capped at the benched quality, axis ratios preserved (Bus Device.QualityChanged(quality, profile)). Client measurement — cosmetics, never authority — DeviceBench

Spec runner and micro-benchmark harness — 505 specs run on every Studio play-test boot. Deep page: TestKit.

Member Description
TestKit.runSpecs(container) → Results Over *.spec modules returning function(T); T.describe, T.it, T.expect(x).toBe/.toEqual/...
Bench.run(name, fn, {DurationSeconds?}?) → {OpsPerSecond, P50Nanos, P99Nanos}
Bench.runAll(container) Over *.bench modules
-- Inventory.spec (ModuleScript)
return function(T)
T.describe("Inventory", function()
T.it("stacks identical items", function()
local Inv = Inventory.new()
Inv:add(7, 1)
Inv:add(7, 2)
T.expect(Inv:count(7)).toBe(3)
end)
end)
end