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.
Kernel
Section titled “Kernel”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 |
ServerKernel
Section titled “ServerKernel”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.
Scheduler
Section titled “Scheduler”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.
Process
Section titled “Process”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 |
Signal / Bus / HookRegistry
Section titled “Signal / Bus / HookRegistry”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 |
Logger
Section titled “Logger”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 |
GcWatch
Section titled “GcWatch”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.
Serde / Base64
Section titled “Serde / Base64”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), CFrameF24U8–CFrameF32U16, 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).
NetDriver (server) / NetClient
Section titled “NetDriver (server) / NetClient”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.
ReplicaService (server) / ReplicaClient
Section titled “ReplicaService (server) / ReplicaClient”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 |
InputDriver (client)
Section titled “InputDriver (client)”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).
DataDriver
Section titled “DataDriver”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.
MemoryDriver
Section titled “MemoryDriver”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 |
Commerce
Section titled “Commerce”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 |
AntiExploit
Section titled “AntiExploit”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 |
Matchmaking / SoftShutdown / ActorPool
Section titled “Matchmaking / SoftShutdown / ActorPool”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.
InventoryKit
Section titled “InventoryKit”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.
LootKit
Section titled “LootKit”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.
CraftingKit
Section titled “CraftingKit”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.
ShopKit
Section titled “ShopKit”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.
CompanionKit
Section titled “CompanionKit”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.
PartyKit
Section titled “PartyKit”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.
TeamKit
Section titled “TeamKit”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?).
MeleeKit
Section titled “MeleeKit”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 |
QuestKit
Section titled “QuestKit”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.
InteractionKit
Section titled “InteractionKit”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.
NPCKit
Section titled “NPCKit”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.
BehaviorTree
Section titled “BehaviorTree”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 |
Senses
Section titled “Senses”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 |
Gameplay systems
Section titled “Gameplay systems”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.PhaseChanged — RoundKit |
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/Cleansed — Effects |
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.Updated — Leaderboards |
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/Ended — Ragdoll |
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/SubtitleEnded — AudioKit |
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 |
TestKit / Bench
Section titled “TestKit / Bench”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