Skip to content

Kits overview

What a kit is, the two shapes kits come in, how kits compose through hooks and the Bus without importing each other, and how they hold rewards while a profile loads.

The kernel contains no game rules. It ships scheduling, networking, sessions, hooks, the Bus and persistence, and stops there. Kits are where game rules live: prebuilt modules in ChloeKernelServer/Kits (and a few client and shared ones in ChloeKernel) that each implement one genre mechanic as server-validated gameplay over those primitives. You attach or register a kit, configure it from Game/Config, and extend it through its hooks and Bus topics. You do not fork it, and AGENTS.md treats reimplementing one as a defect.

Every kit follows the same contract: the client sends requests and the server decides. A config field is a gate the server enforces. FireRate = 8 in WeaponKit means a shot that arrives sooner than 1/8 s (less the jitter allowance) after the last one dies in validation, whatever the client claims.

No kit imports another. They compose through two mechanisms: a fail-open hook for “may this happen?” and the Bus for “this happened”. Both work whether or not anything is listening. One weapon hit, traced through both:

client WK_Fire(weaponId, direction, timestamp)
|
v
Intent.WK_Fire validators (fail-closed)
your rules at priorities below 50, WeaponKit's own check at 50
|
v
WeaponKit handler
Rewind:validateShot the claim is not from the future and not too old
Rewind:castRay the nearest rewound capsule
world ray nothing colliding stands in front of it now
|
v
Weapon.CanDamage hook (fail-open)
TeamKit's friendly-fire veto at priority 40, when FriendlyFire = false
|
v
Humanoid:TakeDamage, then Bus "Weapon.Hit"
|
v
your CombatService: the victim is at 0 health, so it publishes "Combat.Kill"
|
+---------+-----------------+----------------------+
v v v
QuestKit AchievementKit Chronicle, if attached
an objective counts progress or unlock records every publish
(completion waits (unlock waits for and every hook decision
for the profile) the profile)

WeaponKit has no idea TeamKit exists, and TeamKit never touches WeaponKit’s code: the veto is a hook registration on Weapon.CanDamage. WeaponKit publishes Weapon.Hit and nothing else. Combat.Kill is a topic your game publishes (no framework module does), and QuestKit and AchievementKit count it by name. Chronicle is a debug recorder you attach in Studio or behind a flag; once attached, it records every Bus publish and hook decision without any kit knowing.

The same shape repeats elsewhere. SpellKit hands its payload to Projectiles or Effects instead of owning either. CompanionKit spawns NPCKit archetypes and applies its Aura through Effects. A Sweeps hit or a Hazard publishes the way a weapon hit does. Every arrow above is a hook registration or a Bus subscription, addressed by name ("Weapon.CanDamage", "Combat.Kill"). A new reaction (a kill cam through CameraKit, a Discord webhook) subscribes to a topic that already exists, and nothing upstream changes.

Composition below lists the specific kit pairs.

Server kits come in two shapes, and the shape decides where the kit is created.

Kit.attach(kernel, config) returns a handle with methods your code calls. Call it from the owning game service’s init, with the config in Game/Config. InventoryKit, LootKit, CraftingKit, ShopKit, MeleeKit, NPCKit, CompanionKit, TeamKit, PartyKit, DialogueKit and AchievementKit are attach kits.

Kit.service(config) returns a service definition, which the kernel boots, starts and stops like any service. Add it to src/Server/Game/Registry.luau. WeaponKit, SpellKit, QuestKit, RoundKit, CheckpointKit, InteractionKit and ProceduralKit are service kits.

src/Server/Game/Registry.luau
--!strict
-- Every server service, one require per module in Services. ServiceManager
-- orders boot by each service's Dependencies.
local ServerScriptService = game:GetService("ServerScriptService")
local SpellKit = require(ServerScriptService.ChloeKernelServer.Kits.SpellKit)
return {
require(script.Parent.Services.ComponentService),
require(script.Parent.Services.DuelService), -- attaches MeleeKit in its init
SpellKit.service(require(script.Parent.Config.Spells)),
} :: { any }
src/Server/Game/Services/DuelService.luau
--!strict
-- DuelService: owns MeleeKit. Player intents wire themselves; NPC code calls the same methods.
local ServerScriptService = game:GetService("ServerScriptService")
local MeleeKit = require(ServerScriptService.ChloeKernelServer.Kits.MeleeKit)
local Moveset = require(script.Parent.Parent.Config.Moveset)
local DuelService = {
Name = "DuelService",
}
function DuelService.init(self: any, kernel: any)
self.Melee = MeleeKit.attach(kernel, { Moveset = Moveset })
end
function DuelService.stop(self: any)
self.Melee:destroy()
end
return DuelService

A few kits have neither shape:

  • BehaviorTree is a plain helper with no scheduling. Build a tree and tick it from your own loop.
  • MoveKit lives in Shared/ChloeKernel because it has two halves in one module: MoveKit.attach on the client executes abilities, and MoveKit.server on the server meters them.
  • The client and media kits run on the client. CameraKit and CutsceneKit use attach, with CutsceneKit.server on the server to start scenes. MaterialKit uses MaterialKit.new.

Kits stop cleanly. Attach kits have destroy(), which the owning service calls from its stop. Service kits have stop(), which the kernel calls when the server closes. Since 0.9.0 the kits with client intents unhook their validators there and leave their handlers inert, so a destroyed kit’s channels close even when a game validator on the same intent would keep them open. MoveKit’s server meter is the exception: its validator stays hooked and rejects everything once the meter is destroyed.

Four patterns repeat across the kits. Learn them once and each kit page becomes a table of specifics.

Kits that accept client input define prefixed intent channels on the NetDriver: WK_Fire, SK_Cast, ML_Attack, CKMove, INV_Use, CR_Craft, SH_Buy. Kits declare their own channels; they are not in Game.Channels. Each channel has a fixed wire schema and a per-channel rate limit, and every message runs the fail-closed Intent.<Channel> hook chain before any handler. Fail-closed means rejection is the default: a validator returning false rejects, a validator that errors rejects, and a channel with no validator at all rejects every payload. A crash in a check cannot let a message through.

Kit validators run at priority 50 (MoveKit’s at the default, 100). Lower numbers run first, so your game rules go in front without forking the kit:

-- No firing in the lobby. Runs before WeaponKit's own check at 50.
Kernel.Hooks:on("Intent.WK_Fire", function(_context)
local Phase = Kernel:getService("RoundKit"):current() -- a Phase table, nil before start()
if not Phase or Phase.Name ~= "Round" then
return false
end
return nil
end, 10)

Where a kit asks a game-rules question (may this damage land, may this craft start, may this companion come out), it fires a fail-open hook point instead: Weapon.CanDamage, Melee.CanHit, Craft.CanCraft, Inventory.CanEquip, Inventory.CanUse, Companion.CanSummon, Party.CanInvite, Dialogue.CanBegin. Returning false vetoes. A handler that errors is skipped and the action goes ahead.

The asymmetry is on purpose. An erroring intent validator must not let an exploit through, so it fails closed. An erroring damage rule must not silently break all combat, so it fails open. Input trust and game policy fail in opposite directions.

TeamKit shows the pattern: FriendlyFire = false registers vetoes on Weapon.CanDamage and Melee.CanHit, and neither combat kit knows teams exist.

Every kit publishes its outcomes on the kernel Bus: Weapon.Hit, Melee.Parried, Craft.Completed, Inventory.Overflow, Obby.Checkpoint. That is where reactions attach: kill feeds, effect replication, leaderboard submits, and QuestKit objectives subscribe instead of patching kit code. Topics also connect kits that never import each other: QuestKit counts Combat.Kill and Zone.Entered without knowing what publishes them, and Receipts declines a purchase when Loot.Refused or Inventory.Overflow fires for the buyer during its grant.

Kits that hold long-lived player state write it to the session profile when a DataDriver is attached (session.Profile.Data) and to transient session.Data otherwise. InventoryKit bags, LootKit pity counters, CraftingKit timed crafts, QuestKit progress, AchievementKit unlocks, CheckpointKit stages and ShopKit’s default currency field all follow this rule. No kit talks to DataStores directly; loss protection lives in one place.

A player’s session starts before their profile arrives. The DataDriver sets session.ProfilePending = true while it loads the profile and clears it when the load ends, and session.Profile is nil until then. Before 0.9.0, grants, rewards, quest completions and achievement unlocks made in that window were paid into session data and thrown away when the profile loaded. Now the kits hold them:

Kit While ProfilePending is true When Kernel.ProfileLoaded fires
InventoryKit Grants land in a bag in session.Data. Each early slot is granted into the profile bag. What no longer fits publishes Inventory.Overflow. The loaded bag then goes to the owner in INV_Sync, early bag or not.
QuestKit Progress counts in session.Data. Completion, and its OnComplete reward, waits. The merge keeps completed entries and the higher count per objective, then completes what the merged counts finish, so the reward pays into the profile.
AchievementKit Progress counts. Nothing unlocks. The merge keeps unlocks and the higher count per id, then unlocks what reached its target. An achievement the profile already holds never rewards again.
CraftingKit Timed recipes refuse with Loading. Instant recipes work through InventoryKit. Recorded timed crafts complete or resume.
LootKit award items go through InventoryKit and merge. Pity counters go to session.Data. Pity counted in the window is not merged.

Game code follows the same rule: while session.ProfilePending is true, write nothing that must be saved. Per-player setup that needs data waits for the profile with Milestones.on(kernel, "Kernel.ProfileLoaded", fn) from Milestones, which also replays profiles that loaded before you subscribed. Without a DataDriver, ProfilePending is never set and session.Data lasts only the visit.

Kits stack. The pairings to know:

  • InventoryKit is the item layer. LootKit awards its rolls through it, CraftingKit takes inputs and grants outputs through it, and ShopKit sells out of it. Define items once in InventoryKit; the other three refer to ids.
  • A companion is an NPCKit archetype that CompanionKit spawns and has follow its owner. Its optional Aura is an Effects name held by the owner while the companion is out.
  • SpellKit owns mana, cooldowns and channels. What a spell does is your Cast function, usually firing Projectiles or applying Effects.
  • TeamKit vetoes into the combat hooks. PartyKit does not: parties are social, not combat policy, and they feed Matchmaking group queues instead.
  • BehaviorTree trees decide for NPCKit handles. The handle’s helpers, and MeleeKit or Projectiles through movesets, carry the decision out.
  • MeleeKit accepts NPC handles. Register an NPCKit handle as a combatant and it swings and parries under the same rules as players, rolling its own defend against incoming swings.

Server kits:

Kit Shape Client channels Purpose
WeaponKit service() WK_Fire Server-validated hitscan weapons: fire rate and ammo gates, lag-compensated hits against rewound capsules.
MeleeKit attach() ML_Attack, ML_Block, ML_Parry Melee state machine: windup, hit frame, recovery, combos, block, guard break and parry, line-of-sight checked hits.
SpellKit service() SK_Cast Mana, regeneration, cooldowns and channelled casts as killable Processes.
MoveKit attach() on the client, server() on the server CKMove Movement-context abilities (double jump, air step, dash): the client executes, the server meters.
NPCKit attach() none NPC lifecycle, skill model, senses, movesets, squads and cover. Your loop makes the decisions.
BehaviorTree helper none Selector, sequence and decorator trees with no internal scheduling.
InventoryKit attach() INV_Move, INV_Equip, INV_Unequip, INV_Use, the INV_Fetch request (and INV_Sync out) Server-authoritative items: definitions, stacking bags, equip slots, use handlers, a Tool bridge. The client module InventoryClient keeps the latest bag.
LootKit attach() none Weighted loot tables with nesting, per-player pity in the profile, and a Policy gate for paid tables.
CraftingKit attach() CR_Craft, CR_Cancel Recipes over InventoryKit: input takes with rollback, timed crafts recorded in the profile, station gating.
ShopKit attach() SH_Buy, SH_Sell Soft-currency shops over InventoryKit with rotation and stock caps.
CompanionKit attach() CP_Summon, CP_Dismiss Owned NPC followers built from NPCKit archetypes, with owner auras through Effects.
QuestKit service() none Quests as declarative objectives counted off Bus topics the server publishes.
RoundKit service() none (optional Replica out) The round loop: an ordered phase cycle with deadlines, player gates and a replicated countdown.
CheckpointKit service() none (server-side Touched on pads) Validated obby progression: sequence, minimum time and pad reach checks, saved in the profile.
TeamKit attach() none Team assignment, team spawns and friendly-fire vetoes in the combat hooks.
PartyKit attach() PT_Invite, PT_Accept, PT_Decline, PT_Leave, PT_Kick, PT_Promote (and PT_Sync, PT_Invited out) Same-server parties: invites that expire, leadership, roster pushes, Roblox platform parties.
InteractionKit service() ProximityPrompt triggers through Intent.Interact ProximityPrompts on tagged instances, with distance, line of sight and cooldown checked on the server for every trigger.
ProceduralKit service() none (wire yours through service:validate) Runtime ProceduralModel spawning with fail-closed parameter validation and rate-limited regeneration.
AchievementKit attach() none Conditions over the Bus with saved progress, one unlock per player ever, and platform badges.
DialogueKit attach() DLG_Choose, DLG_Advance (and DLG_Sync out) Server-authoritative conversation trees: the client renders what it is sent and answers with an index.

Client and media kits:

Kit Shape Channels Purpose
CameraKit attach() on the client none Cinematic camera control as data: shots, blends and rails, bound to one render step while it holds the camera.
CutsceneKit attach() on the client, server() on the server CKCUT_Play (server to client) Timelines of camera shots, animations, sounds, effects and dialogue lines that every client plays locally from one packet.
MaterialKit MaterialKit.new() on the client none Real-geometry surface dissolution over EditableMesh.
AnimKit attach() none Registered animations per rig: crossfades, exclusive groups, markers on the Bus, and an IK layer.
AudioKit attach(), with server() and listen() for plays the server starts CKAudio (server to client) 2D and 3D audio over mixer SoundGroups, with fades, ducking, pooled sounds and adaptive music layers.
HairKit attach() on the client, server() to rig on the server none Automatic rigging for boneless hair accessories, swayed by BonePhysics.

The rest of Client & Media (InputDriver, VfxSuite, Impact, Gait, Highlights and the others) are modules rather than kits; start at InputDriver.

The starter templates are complete small games. Each server file lists its map requirements at the top.

Template Kits exercised Alongside
GunArena WeaponKit, TeamKit, RoundKit Rewind lag compensation, Leaderstats, Leaderboards
WizardDuel SpellKit, MeleeKit, MoveKit Effects curses and wards
Obby CheckpointKit, MoveKit Zones and Touched kill bricks, a Leaderboards speedrun board

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.