Changelog
Tagged versions ship a built .rbxm on the GitHub Releases page — insert it into a place to pin an exact framework version instead of syncing main. Pushing a v* tag builds the artifact and cuts the release from the matching changelog section automatically.
Unreleased
Section titled “Unreleased”- HairKit: automatic hair rigging over EditableMesh — boneless catalog hair gains a generated skeleton at runtime:
plan()partitions mesh vertices into radial sectors x depth bands and emits a bone chain per sector (default 10 bones) with every vertex weighted to at mostMaxInfluencesbones (default 2, clamped to 4, weights normalized; root-plane vertices bind the root and never pull band centroids);rig()injects the skeleton (AddBonewithParentId/Virtual,SetVertexBones,SetVertexBoneWeights), registers the content into the DataModel (CreateDataModelContentAsync(Content.fromObject(mesh))), bakes throughCreateMeshPartAsync, swaps the hardware-skinned result into the MeshPart in place, and creates the Bone instance skeleton the mesh links to by name (write-once, read-many — the baked editable stays rooted for the part lifetime and is never mutated again). Sway rides BonePhysics (its single stepper, camera-distance culling, DeviceBenchMaxChainsbudget).attach()= client bulk manager (no per-accessory scripts);HairKit.server()rigs server-side so the baked bones replicate to every client once — zero per-frame network traffic in either mode. Two permission gates verified live: the “Allow Mesh & Image APIs” experience setting (supported()probes it with a realAddBonecall) and per-asset read permission (CreateEditableMeshAsyncloads experience-owned or creator-owned meshes only; third-party catalog hair returns(false, reason)); either failure keeps thebindAccessorypendulum fallback. BusHair.Rigged. See HairKit - Dissolve: vertex-sampled voxelizer with noise-advected dissolve, restructure, and particle morphs — MeshParts sample EditableMesh vertex positions (surface-shell fallback with Ball/Cylinder silhouette filtering — interior cells drop so nothing dissolves as a filled box; permission denials cached per asset so avatar effects warn once per session), home positions ride the target’s anchor part so reassembly, restructure, and morph endpoints follow moving targets (a player who walks off mid-effect reforms where they are), points snap to a
DotSizevoxel grid with per-cell dedup, and dots advect through a 3D Perlin field (ScatterSpeed,NoiseScale,Drift) while fading;Reverseassembles instead;Duration = nilreturns a manual controller whosesetProgress(0..1)erodes the surface noise-thresholded per dot, and dots above the threshold ease back to their surface cell and resolidify (ReturnSpeed) so a receding driver restructures the target (proximity wards that knit shut).morph(source, destination)voxelizes source and flies every dot into destination’s sampled shape — bottom-up pairing (pairPoints, every destination cell used), staggered launches (Stagger), Perlin turbulence arcs (Turbulence), source-to-destination color lerp — then reveals destination and keeps source hidden; avatars are first-class targets (Decals and Textures hide with their parts): disintegrations, character-to-statue morphs, reforming sources viaReverseplays; chained effects restore the pre-effect transparency, never a mid-chain hidden value.Chunks = true(play and morph) clones every source part as a full-detail flying chunk — meshes, textures, faces, and SurfaceAppearance survive, joints/welds/sounds/emitters strip — riding the same threshold, advection, or morph flight while shrinking into the dust, which keeps original avatar detail where denied-mesh grid sampling would box it; chunks destroy on finish. One Heartbeat stepper across every active effect, pooled dots positioned throughworkspace:BulkMoveTo,MaxPointsscaling 500..5000 by DeviceBench quality when unconfigured,CustomDottemplates,Spin. Local-only VFX; broadcast the trigger for shared playback. See Dissolve - Starter templates — the repo’s
templates/directory ships three complete minimal games as reference code (GunArena: WeaponKit + Rewind + TeamKit + RoundKit + Leaderstats + global boards; WizardDuel: SpellKit + MeleeKit + MoveKit + curse/ward Effects; Obby: CheckpointKit + air steps + kill-brick Zones + ascending speedrun board). One server module and one client module each; linted in CI alongsidesrc. See Starter templates - Studio boot gates in Main.server — every Studio play test now asserts zero High-severity
securityAuditfindings after start and runsFuzz.runagainst every registered channel when the first session joins; handler errors fail the play test loudly. See Fuzz & audit - StreamingEnabled stance documented — from a module audit: server-authoritative systems are unaffected; client-side rules for BonePhysics binds and long-lived model references. See StreamingEnabled
Changed
Section titled “Changed”- API consistency: kernel-integrating modules use attach() —
InputDriver.attach(kernel)andMoveKit.attach(kernel, options)are the canonical constructors;.newremains on both as a deprecated alias. Plain object constructors (Pool.new,NetClient.new) and service definitions (WeaponKit.service) keep their shapes: the rule is attach = wires into a kernel, new = plain object, service = definition forregisterService
0.5.0 — 2026-07-08
Section titled “0.5.0 — 2026-07-08”- PartyKit: same-server player parties — invites with TTL expiry (sweep-cleaned), one party per player (accepting leaves the current party), lazy creation (the party forms on the first accept with the inviter as leader), leader kick/promote, leader inheritance on leave (longest-standing member), disband at zero members, session-end cleanup of membership and pending invites both ways. Six fail-closed intents (
PT_Invite/Accept/Decline/Leave/Kick/Promote, UserIds as F64 on the wire),PT_Syncroster pushes andPT_Invitednotifications,Party.CanInvitehook (fail-open). Parties never touch the combat gates;samePartyandmembers(group-queue feed for Matchmaking) are the integration surface. BusParty.Created/Disbanded/Joined/Left/Kicked/Invited/Promoted. See PartyKit - Effects is now a full status system —
TickSeconds+OnTickperiodic ticks (poison, regen) with catch-up bounded to 8 per sweep;Categorydispel groups withcleanse(session, category?)(no category = every categorized effect; uncategorized effects never cleanse);Immune = {categories}on an effect blocks covered applies while it holds (wards are effects;applyreturns 0 and publishesEffects.Blocked); owners receive aCKEffectsstate push after every change ({[name] = {Stacks, Remaining?}}) for status-icon UI. Bus addsEffects.Blocked(player, name, byEffect)andEffects.Cleansed(player, category?, count). See Effects - LootKit: weighted loot tables with persisted pity — entries yield items (
Countfixed or{min, max}), nested tables (depth capped at 8, references validated at attach), or nothing;PityAfter = Nforce-hits on the Nth consecutive miss with counters persisted per player in profileData.LootPity(rollFor/awardadvance pity, plainrollnever does);award(session, table)grants through InventoryKit and returns(granted, overflow). BusLoot.Rolled/Awarded. See LootKit - CraftingKit: recipes over InventoryKit — inputs check atomically before any take;
Seconds > 0queues one timed craft per session withcancel()refunds;Stationrecipes gate on the caller’s claimed station with proximity verified in the fail-openCraft.CanCrafthook; output that does not fit publishesCraft.Overflow; fail-closed intentsCR_Craft {recipeId, station}/CR_Cancel. BusCraft.Started/Completed/Cancelled/Overflow. See CraftingKit - ShopKit: soft-currency shops over InventoryKit — currency is an adapter (
{Get, Take, Give}; default reads a profile field,CurrencyField = "Coins"); rotating stock derives fromSeed + floor(clock / Every)so every server lists identical stock with zero coordination;Stock = Ncaps buys per player per rotation window; buys grant first and charge for what fit (partial grant = partial price, full inventory = no charge); fail-closed intentsSH_Buy/SH_Sellwith counts capped at 99. BusShop.Bought/Sold/Rejected. See ShopKit - CompanionKit: owned NPC followers — familiars/pets from NPCKit archetypes: one per session (re-summon replaces), a follow loop that walks at
FollowDistanceand pivots beside the owner pastTeleportDistance, death cleanup withCompanion.Died, andAuraholding an Effects buff on the owner while summoned; ownership gates in the fail-openCompanion.CanSummonhook; fail-closed intentsCP_Summon/CP_Dismiss. BusCompanion.Summoned/Dismissed/Died. See CompanionKit - Fuzz: hostile-payload sweep over every registered channel — the runtime companion to
securityAudit(): per-schema-type hostile pools (empty/10KB strings, format specifiers, control bytes, 0, -1, type maxima, NaN, infinities, NaN vector components, nil/nested tables for Any) drive each intent and request through the real hook chain + handler pipeline; payloads that pass validation and crash the handler collect inReport.HandlerErrors(assert it empty as a boot gate); validator throws count as rejects; deterministic perSeed;Driverinjectable for specs. See Fuzz & audit
0.4.0 — 2026-07-08
Section titled “0.4.0 — 2026-07-08”- InventoryKit: the item model — definition registry + slotted per-player bags: stacking (grants top up piles before opening slots), equip slots with displacement and
OnEquip/OnUnequip,OnUseconsumables (return true to burn one; draining a stack auto-unequips),movedrag-and-drop (swap or merge, equip references follow the item). State persists through the session profile (Persist = falsefor session-only battle-royale bags). Grants/takes are server-API-only — no client message mints items; clients ride four fail-closed intents (INV_Move/Equip/Unequip/Use) validated against the server’s books, and the owner gets anINV_Syncstate push after every mutation so inventory UI is pure rendering. HooksInventory.CanEquip/CanUse(fail-open); BusInventory.Granted/Taken/Equipped/Unequipped/Used/Changed. See InventoryKit - Leaderboards: global top-N on OrderedDataStores — queued submits flush on a cadence (newest value per key per window = one write no matter how fast scores change),
KeepBestboards only overwrite improvements through atomicUpdateAsync(Ascending flips the comparison for speedruns), failed writes stay queued and retry next window, andtop()serves a cached page (oneGetSortedAsyncperCacheSecondsregardless of readers; failed refreshes keep serving the previous page). The backend is a first-class seam — Roblox OrderedDataStores by default, custom stores as equals: implement declarativesubmit(board, key, value, keepBest, ascending)(the keep-best rule arrives as data, so an external DB applies it atomically its own way) or DataStore-shapedset(board, key, updater), plussorted; contract asserted at attach.WritesPerFlush(default 30) caps each window so provider budgets (ODS limits, HTTP rate caps) survive submit spikes — overflow keeps its queue spot. NaN/inf submissions refuse; BusLeaderboard.Updated(name, entries). See Leaderboards - TeamKit: teams wired into the combat gates — capacity-weighted auto-balance on session start,
assign/teamOf/sameTeam/players, optional Roblox Teams mirroring for player-list colors, and map-authored spawn ownership (tag partsTeamSpawn+Teamattribute; characters pivot to an owned spawn with the anti-exploit pardoned). Friendly fire off by default, enforced where damage happens: vetoes registered onWeapon.CanDamage(new fail-open hook in WeaponKit’s damage path) andMelee.CanHit; npc handles resolve throughArchetype.Faction, so faction NPCs and same-named teams read as teammates. BusTeam.Assigned(player, team, previous?). See TeamKit - WeaponKit:
Weapon.CanDamagehook point — fail-open gate fired before a validated shot deals damage ({Session, Attacker, Victim, WeaponId, Weapon}); TeamKit plugs friendly-fire policy in here, games add safe zones and duel rules without forking the kit. See WeaponKit
Changed
Section titled “Changed”- Repo-wide comment rewrite — every comment states contracts, constraints, units, defaults, and data shapes; rhetorical framing removed; option comments normalized to
-- <fact>. Default <value>; ~680 comments across 118 files with the code proven byte-identical by a comment-stripping diff; three stale factual claims corrected against the code (Net pipeline order, ServiceManager cycle behavior, AnimKit IK helper class)
0.3.0 — 2026-07-08
Section titled “0.3.0 — 2026-07-08”- MeleeKit: server-authoritative hand-to-hand with parries — attacks are windup / hit-frame / recovery timelines on the SERVER clock; combos cancel recovery along declared
ComboNextchains (Melee.Combocounts the string); blocking chips (BlockScale), guard-breaks shatter blocks (full damage, guard drops), and timed parries beat both: a parry press arms aParryWindow(+LatencySlackso wire travel doesn’t eat honest frames) that kills the incoming swing and staggers the attacker (StaggerSeconds, x1.5 for parried guard-breaks — the triangle: parry > guard-break > block > attack > parry). Attempts consumeParryCooldownhit or miss, so spam can’t blanket a swing. Hit frames sweep range + facing arc with a fail-openMelee.CanHithook for friendly-fire policy. Players ride auto-wired fail-closed intents (ML_Attack/ML_Block/ML_Parry— “attack while staggered” dies in validation); combatants are entity-agnostic (sessions, npc handles, models), and npc victims with no manual parry roll their own skill-scalednpc:defend()— difficulty-driven NPC parries for free. Bus:Melee.Hit/Blocked/GuardBroken/Parried/Staggered/Combo/State. See MeleeKit - MoveKit: abilities bound to movement context — “jump pressed while airborne” as a first-class trigger: abilities are a trigger (
Input = "Jump"ridesUserInputService.JumpRequest, other strings ride InputDriverInput.<Action>bus events, orkit:trigger(name)) plus a context gate (Grounded/Airborne/Rising/Falling/custom predicate over{State, AirTime, Rising, UseIndex, ...}), a charge pool that refills on landing, a cooldown, and a takeoff debounce (MinAirTime). Recipes:doubleJump({Power})(fresh scaled jump mid-air) andairStep({Steps, Boost, OnStep})(anime footholds — kills the fall, hops off the invisible platform,OnStep(position, useIndex)draws the glow pad). Ownership guarantee intact: the CLIENT executes (it owns its character’s physics),MoveKit.servermeters through the fail-closedCKMoveintent with its OWN charge/cooldown books and its OWN ground probe (a client that lies about landing gets no fresh charges);Forgive = truepardons the next anti-exploit sample for horizontal bursts. Bus:Move.Triggered(client),Move.Used/Move.Rejected(server). See MoveKit - ProjectileClient fairness rule: no invisible shots —
MaxVisualsnow caps only the FULL cosmetic visual; overflow spawns render as minimal pooled neon tracers instead of being skipped, and any shot whose flight path passes withinThreatRadius(default 15 studs) of the local character renders FULL regardless of the cap — an incoming round the target can’t see is a fairness bug, not an optimization.MaxTracers(default 300) bounds pathological spam (threatening shots bypass every cap). New pureProjectileClient.threatens(origin, velocity, position, radius)+ spec. See Projectiles - DeviceBench: granular per-axis device benchmark, computed quality budgets, and a 60fps governor — three time-boxed Luau workloads (compute, allocation churn, coroutine resumes) measure how fast the client actually runs, and each becomes a CONTINUOUS 0.25..4 axis multiplier against a mid-range reference, decimals fully allowed; the geometric mean is the overall
Quality. Budgets are FUNDED PER AXIS — the workload a device is good at pays for the systems that stress it: strong Churn buysParticleBudget/VfxDensity, strong Compute buysBoneChains/BoneDistance/ProjectileVisuals, strong Resume buysAudioChannels— so a device that benched 3.4x on churn but 1.2x on compute gets near-max particles while bone chains stay modest.profile()computes the budgets from a Result, any exact quality number (profile(1.35)), or a tier name;Low/Medium/High/Ultratiers remain coarse conveniences forpick(). Client modules consume the profile when unconfigured: BonePhysics cullMaxDistanceand NEWMaxChainschain budget (past it the chains nearest the camera win, the rest freeze at their pose), AudioKitMaxChannels, ProjectileClient NEWMaxVisuals(pooled-render cap; hits still land past it). Every default is an overridable option and every cap is purely cosmetic.scale(min, max)is log2-mapped over the band — every doubling of device quality buys the same slice of the knob. The GOVERNOR holds a stable frame target at runtime — 60fps first, beauty second: it watches p95 frame times and nudges the effective quality ×0.8 after ~3s of sustained misses, ×1.15 back after ~10s of clear headroom — granular nudges, not tier cliffs — capped at the benched quality (frame luck is not capability), with axis ratios preserved while dialing (a churn-strong device dialed down still favors its particles); changes arrive viaOnChange(quality, profile)andDevice.QualityChanged. Time-boxed means weak devices run fewer iterations in the same ~90ms instead of hitching longer; results cache, reportPlatform, optionally sample a medianFrameMs, publishDevice.Benchmarked, and auto-run on first helper use — pay the bench during loading (pairs with Preload: asset warming is network-bound, the bench uses the idle CPU). A client measurement — cosmetics only, never authority. Reference constants are provisional pending a live calibration pass. See DeviceBench - NPCKit is now HELPERS, not an AI runtime — the behavior system is gone: no priority-list
{When, Step}runner, no kit-owned tick loop, no built-in behavior factories (wander/idle/chase/investigate/hide/cover/strafe/attack/suppress/flank/flee/tree all deleted). The kernel ships syscalls, not game rules — and chase/cover/flank policy IS game rules. What remains is a thin spawn/despawn lifecycle (model clone, health, died-cleanup, zones tracking) plus per-npc state, and YOUR loop composes the helpers:npc:updateTarget(acquisition/sight memory/graded detection),npc:canSee/npc:sweep(cone-gated eyes, pie-slice fans),npc:aimAt(the full skill model),npc:act(movesets),npc:defend,npc:moveTowards/npc:faceTowards,kit:findCover/kit:findPeekPoint(cover SCORING without the phase machine — Direction-attribute nodes, BackAway ground-giving, squad spread all kept),kit:all()(the roster your loop walks), andkit:update()(squad director + due melee windups, called from your tick). Two new standalone helper modules:Senses— hearing with levels of ATTUNEMENT (Keen/Sharp/Average/Dull/Obliviousor any custom{Range, BlurStuds}), anisotropic blur (direction resolves well, distance poorly), and aDistanceoverride so route-measured occlusion plugs in; plusinCone/canSeevision gates — NPCKit’s own ears and eyes delegate here, so kit npcs and hand-rolled agents perceive identically.Pathfinding.follower— per-agent re-pathing/waypoint-following as a handle your loop steps (follower:step(position, goal) -> nextPoint, {Path?, Jump, Stuck}): goal-drift/grid-version re-paths, failed-search backoff, stuck watchdog, jump hints;npc:moveTowardsis now a thin wrapper over it and applies the point via Humanoid/injected Move.BehaviorTreestays as the pure strategic helper — build one per npc, tick it from your loop with the kit clock. See NPCKit, Senses, Pathfinding - NPCKit trained-combatant pass: graded detection, pain response, ammo discipline, honest gunfights — sight can now charge instead of snapping:
DetectionSecondsonupdateTargetturns acquisition into an accumulator (fills faster up close and against fast movers, decays unseen; half charge publishesNpc.Suspiciousand points the hearing memory at the glimpse so your search logic walks over for a look, full charge acquires), and re-sighting a target after real occlusion re-imposes HALF the reaction time — the reappearance is a fresh stimulus, not a free instant re-engage. Getting hit registers:npc:notifyDamage(attacker?)(called automatically by kit Hitscan/Melee on npc victims, wired to Humanoid damage on spawned rigs, published asNpc.Damaged) suppresses aim for 1.5s (readState.SuppressedUntilto duck an exposed peek) and drops an unseen attacker’s position into the hearing memory — a back full of bullets now turns the body around. The aim model grew up: leading uses the TRUE intercept solve (|delta+v·t| = s·t) instead of distance/speed, error is correlated drift + near-Gaussian jitter (bursts walk readably instead of statistically bracketing the target), continuous time-on-target tightens the cone, and suppression blooms it. Movesets:Magazine/ReloadSecondsforce the reload pause players push into (Npc.Reload), meleeWindupSecondstelegraphs the swing clock-driven with a real dodge window (Npc.Windup; landing re-checks the arc), and ranged actions in a squad HOLD FIRE when a squadmate blocks the lane (FriendlyFire = trueopts out).npc:faceTowardstracks targets with the body while standing still — a stopped npc’s frozen vision cone would otherwise lose a target circling behind it. The squad director fields real fireteams — nearest holds Pressure, the rest alternate Flank (sides split so the pincer closes from BOTH directions,Squad.FlankSigns) and Suppress (base of fire) — and the blackboard expires 30s-stale intel so dead men stop being suppressed and departed Players stop being retained - Pathfinding: feet-vs-photons split, one-flood hearing, off-mesh links, unstickable movers — grid line of sight split into two honest queries:
lineOfSight(walkability) now also gates CONSECUTIVE cells along the line byMaxStepHeight, so a Theta* shortcut between two in-band endpoints can no longer walk an agent off a sheer cliff a stepwise search would refuse, while newsightLine(vision — now backingNPCKit.hasLineOfSighton grids) lets eyes cross valleys and pits that block feet and only occludes on ground RISING past the band. NewPathfinding:distanceField({Grid, Origin, MaxDistance})Dijkstra-floods route distances from one origin —emitSoundPath occlusion now runs ONE search per sound instead of one full A* per listener.grid:addLink(fromWorld, toWorld, cost?)authors jump/vault edges (ledges, windows) that A*/Theta* traverse but never Theta*-shortcut across; humanoid movers jump for waypoints rising past step reach. Movement robustness: failed searches back off 1s instead of re-burning the full expansion budget every tick against an unreachable goal,Grid.Version(bumped byrefresh/refreshRegion) invalidates cached npc routes when the map moves under them, a stuck watchdog (no ground covered for a second while trying) forces a repath plus an unstick hop, andPathMethod = "Roblox"computes off-tick —ComputeAsyncyields could previously stall the entire npc population from one navmesh brain. Also: any-angle search uses the euclidean heuristic (octile overestimates euclidean segment costs — inadmissible), the ground raycast setsRespectCanCollideso foliage decor stops rasterizing as floor, and waypoint arrival radius scales with cell size. The default rasterizer stopped reading transient BODIES as geometry — a cell probed while a player or npc stood on it cached as blocked (or as walkable ground at head height) forever, including the asker’s OWN start cell — and its clearance column now starts at step height instead of the floor, so a 1-stud lip or shin-high debris no longer dead-strips the ring of cells around it (a SpawnLocation’s edge used to island the whole spawn platform). Newgrid:nearestWalkable(position, maxCells?)— the forgiving front door for user-driven queries: clamps off-grid points into bounds and snaps clicks on top of walls to the nearest standable cell, ring by ring, instead of failing onGoalOutsideGrid/GoalBlockedtechnicalities. The clearance check also grew a NARROW PHASE:GetPartBoundsInBoxmatches BOUNDING boxes, and a wedge ramp’s bounding box is a full-height slab that blocked every cell of its own slope — non-block shapes (wedges, cylinders, meshes, unions) now confirm against real geometry with footprint-sampled down-rays, so ramps rasterize as the walkable slopes they are. See Pathfinding - BehaviorTree: reactive preemption + honest clocks — new
ReactiveSelectorre-evaluates from the FIRST child every tick so a higher-priority branch coming alive preempts a Running lower one (whose resume memory resets) — the right root for combat brains, where “enemy appeared” must interrupt “investigate” mid-plan; plainSelectorkeeps the resume-directly semantics.tick()threads an optional clock intoCooldownnodes (pass the NPCKit kit clock for determinism under spec, without the global_setClockswap), andreset()is the documented move on target change — a plan mid-flight against the old target is a bug against the new one. See BehaviorTree - NPC determinism + correctness touch-ups —
defend()rolls reactions in name-sorted order (hash-order iteration made WHICH reaction answered nondeterministic under a fixed Seed), hitscan/sweep victims resolve through nested prop models to the HUMANOID-bearing ancestor, and npcs ignore their own noise —Sounds.Topicscan safely listNpc.Hitscanso NPC gunfire alerts OTHER npcs without the shooter investigating itself - NPCKit tactical layer: squads, directors, and behavior trees — archetypes take
Faction(faction-mates never target each other) andSquad(auto-join a shared blackboard): members report what their senses ACTUALLY produced (sight fixes exact, heard positions blurred by the listener’s skill) viasquad:report, readable throughsquad:lastKnown(target)/npc:lastIntel(target); the per-squad director (stepped bykit:update()) allocates roles per engaged target, publishingNpc.Tactic(npc, role)for client animation polish — your loop readssquad:role(npc)to decide who pins the last-known fix and who curves the arc. Cover is map-authorable: tag parts (SpotTag) with an optionalDirectionattribute so a node only counts when the threat is on its covered side, and squadmates already camping a node push its score down.npc:sweep(direction, arc?, rays?, range?)slices the pie — a horizontal raycast fan across the unseen angle before the body commits. The vision cone gate is a dot-vs-cos compare that runs before every sight raycast. NewKits/BehaviorTree:Selector/Sequencecomposites with realRunning-resume memory,Condition/Actionleaves,Invert/Succeed/Cooldowndecorators,reset(), injectable clock — build one per npc so plans never share memory, tick it from your loop - NPCKit combat brain: vision cones, sight memory, defense reactions, cover scoring — sight is a cone, not a sphere: new
FieldOfViewDegreesdifficulty knob (Perfect 360 → Bad 100) gatesnpc:canSee(target, maxRange?, fov?)against the rig’s actual facing before any raycast (hearing stays omnidirectional, covering the rear);RequireSighttargeting tracks briefly-occluded targets forMemorySecondsthen hands the LAST SEEN position to the hearing memory — your loop hunts the spot like a soldier instead of pathing omnisciently through walls.kit:findCover(withBackAwayfor fighters that give ground) breaks the target’s line of sight from grid cells or tagged nodes;kit:findPeekPointfinds the corner beside a spot with eyes on the threat. Defense: per-archetypeReactions(Againstattack types, per-reactionDifficulty, cooldown consumed on ATTEMPT) rollDefenseSkill— kitHitscan/Meleeactions roll the victim’s reactions automatically when it’s an npc,npc:defend(context)exposes the same gauntlet to game code, andNpc.Defense(npc, name, success, context)publishes deflects AND fumbles. Per-actionDifficultyoverrides resolve at define-time (Perfect defense on a Novice trigger finger); anisotropic hearing blur stretches error along the sound direction (direction resolves well, distance poorly) - Ragdoll v2: rig-agnostic, build-once, owner-client replication — smooth down, clean recovery — NEW-format character rigs (AnimationConstraint joints, zero Motor6Ds) ragdoll by cutting the animated joints and TUNING the rig’s own ball sockets for the ride (friction via
FrictionTorque(default 60) + limits, everything restored on release) — frictionless free-spinning joints were the first “spazzing ragdoll”; classic rigs keep the Motor6D→limited-socket swap, now also friction-damped. The residual spazz had two more causes, both fixed: the owner client now LATCHES the Physics state (auto-getup disabled + a StateChanged re-force — ground impacts knocked the state machine into Landed/GettingUp, whose standing forces wrestle the sockets) and limbs ride an auto-registered SELF-BLIND collision group (CKRagdollLimbs: jointed-pair no-collides missed hand-vs-hip and arm-vs-leg contacts, whose depenetration impulses shook the pile). No automatic force at all: a socketed stack loses its balance the moment the joints cut and crumples on its own — explicitImpulsehit flings distribute per part, since with rigid joints cut every limb is its own assembly and impulses on the massless ghost root moved nothing. DefaultFrictionTorquerelaxed 60 → 10 (the high value was masking the state-machine and self-collision spazz, both now fixed structurally), and joint cones widened to anatomical ranges — shoulders 120, hips 90, elbows/knees 80 (tight cones read as a stiff mannequin; limits only stop the impossible). NewOptions.Limitsmerges per-joint{Upper, Twist}overrides over the built-in table, so the flop is tunable per game. The ROOT joint itself is never cut: the HumanoidRootPart is the camera subject, and cutting its joint orphaned a massless, non-colliding ghost that fell through the world — the camera dove after it, and release “recovered” the character under the map; left attached, the root rides the torso and the camera lies down with the body. Recovery uprights the root +2 studs (it lies at torso height; standing needs leg room or GettingUp unfolds into the floor). Components build ONCE per model and toggle on re-ragdolls (no instance churn on the wire). Player characters: humanoid state is client-authoritative, so the server pushes theCKRagdollstate channel andChloeKernel/Net/RagdollClient.listen(netClient)answers on the owning machine; release WAITS for the server’s joint re-enables to replicate (standing up while joints were still cut was the broken recovery), kills linear AND angular momentum, stands the root upright with yaw kept, re-enables Animate fresh, and gets up — server-simulated rigs get the same momentum-kill + upright on their release. PlatformStand applies only to server-simulated rigs — stacking it on a player character made the two controllers fight. Verified live: state holds Physics the whole ride, torso angular velocity settles to zero within a second of the kick, recovery stands upright (UpVector.Y = 1.00) straight into Running. See Ragdoll - Pathfinding: partial grid refresh —
grid:refreshRegion(minWorld, maxWorld)drops only the cells a moving wall or opening gate swept; the rest of the map keeps its raster, so continuously-moving courses re-path cheaply (the fullrefresh()stays for map-wide changes) - BonePhysics: boneless accessory sway + ambient wind —
bindAccessory(run automatically bybindCharacter) makes accessories WITHOUT bones move: the accessory becomes a swing-clamped pendulum hinged at its joint, steered through whichever joint kind wears it — Weld/Motor6D handle-side offsets, RigidConstraint handle-side Attachment CFrames (the modern catalog-hair case; nothing swapped or disabled), or WeldConstraints swapped for an equivalent local Weld and restored exactly on release.MaxSwingDegreeskeeps hair drifting instead of flipping. NewAmbientWind = Vector3sim option: a procedural breeze gusted by layered sines, no Wind function needed. Verified live on a real avatar: 10/10 accessories bound, hair swaying through its untouched RigidConstraint. See BonePhysics - NPCKit: opt-in NPC-vs-NPC —
Archetype.Targetable = truejoins the default target roster so other npcs hunt that kind through ordinary acquisition (self-targeting excluded, faction-mates filtered); overrideGetTargetsfor full roster control - AudioKit: the kernel’s all-in-one audio engine — bank-registered 2D/3D playback over auto-created bus SoundGroups (Music/SFX/Ambient/Voice/UI), with every fade, crossfade, and duck driven by ONE scheduler stepper (no TweenService churn, no per-sound Heartbeat connections, deterministic under spec). Sound instances and 3D emitter parts pool; channel pressure evicts lowest-priority-first (music is never evicted); ducks stack with deepest-wins semantics. Adaptive music mixes layered stems by WEIGHT on one locked timeline (
playLayers+setWeights/resync); cue points fire loop-awareAudio.Cuebus events;speak()queues dialogue on the Voice bus, auto-ducks, and publishesAudio.Subtitle/SubtitleEndedfor the game’s UI; reverb zones apply by listener position with priority overlap; soundscapes scatter randomized chirps over an ambient bed. Kernel-only integrations:Alertrings NPCKit ears so player-audible sounds are also NPC intel,Occlusion = "Path"muffles by the pathfinding route around geometry (walls block, corridors carry),bindSettingspersists bus volumes through the validated Settings service, andAudioKit.server/audio:listenbroadcast plays over one state channel. See AudioKit - AnimKit: the animation counterpart — bank-registered clips played through per-rig controllers with track caching, crossfading exclusive
Groups (walk -> run without the T-pose blink), marker-to-bus forwarding (Anim.Marker), and an inverse-kinematics layer:rig:ik()wraps IKControl (LookAt/Reach/Transform/Rotation) with stepper-faded weights, Vector3 targets ride a movable helper,Propertiespasses raw overrides, andrelease()hands the skeleton back. Rigs die with their models;assetIds()feeds the preloader. See AnimKit - Preload: the framework’s asset pool — collects every registered asset (
AudioKit/AnimKitbanks viaassetIds(), plus extra ids and whole Instances), dedupes, and warms them in batchedPreloadAsynccalls off-thread, narratingPreload.Started/Progress/Doneon the bus for loading screens;await()blocks a boot flow until warm; failures collect and report, never abort. See Pool & Preload - BonePhysics: a lean, leak-free SmartBone replacement (
ChloeKernel/BonePhysics, client-oriented shared module) — verlet chains over Bone hierarchies with a FIXED-timestep solver (hitch-clamped substeps; no frame-rate-dependent droop), flat arrays instead of per-bone objects, camera-distance culling that snaps back to the pose on wake, and a teleport guard so respawns don’t whip chains across the map. Poses write throughBone.Transform(the animation slot) composed against the parent’s effective world — neverWorldCFrame, whose setter permanently mutates the bone’s rest CFrame (verified empirically; that mutation is exactly the “SmartBone broke my skeleton” class of bug). Lifecycle is airtight: roots auto-unbind onDestroying,handle:destroy()restores Transforms,sim:destroy()releases everything.bindCharacterscans accessories/layered clothing for boned parts;bindPartsdrives plain part chains (stud tails) with the same solver. Settings per bind:Damping/Stiffness/GravityScale/WindScale; sim options include injectableWindandShouldSimulate - Ragdoll: Motor6D-to-BallSocket ragdolls with per-joint swing/twist limits (R6 + R15 names), collision flips (limbs drape, root goes ghost + massless), optional death wiring,
Impulseflings,Durationauto-release, and aMaxActivecap that releases the oldest pile-up.release()restores EVERYTHING it touched — motors, collision flags, collision groups,AutoRotate,RequiresNeck, humanoid state. Replication is near-perfect at zero extra cost: the joint swap is server-side and the engine streams the physics; a player’s own character keeps its network ownership (per the kernel’s no-ownership guarantee), so their ragdoll simulates locally at full rate. Bus:Ragdoll.Started/Ended(model) - NPCKit combat model: hitscan, arcs, live difficulty —
Kind = "Hitscan"actions resolve through an injectable raycast with aim error only (no lead — it’s instant) and publishNpc.Hitscan(npc, origin, hitPosition, victim?)tracers;Action.Gravityruns a ballistic low-arc solve for lobbed projectiles (out-of-range lobs 45 degrees);npc:setDifficulty(nameOrTable)swaps the skill model mid-fight - NPCKit routing skill + path events — new
PathSkilldifficulty knob (Perfect 1.0 → Bad 0.2): high skill takes any-angle Theta* and re-paths eagerly as goals drift, low skill runs plain A* and clings to stale paths; explicit kitPathMethodstill overrides. Every computed route publishesNpc.Path(npc, waypoints)so visualization/debug tooling never monkeypatches the finder - NPCKit senses: hearing —
emitSound(position, {Range?, Loudness?, Source?})alerts NPCs with range AND accuracy scaled by difficulty (newHearingMultiplier/HearingBlurStudsknobs on every preset:Perfectpinpoints the exact position,Badhears a vague 30-stud blur — the ear model itself lives in the standaloneSensesmodule),Sounds.Topicsauto-emits from listed bus topics at the acting player’s position ({["Weapon.Fired"] = 60}= gunshots make noise with zero wiring), andSounds.Occlusionpicks the geometry model:"Through"(walls ignored, default),"Blocked"(walls stop sound — the “through-wall stuff off” switch), or"Path"(distance measured AROUND obstacles on the grid, maze-correct; no route = not heard). The heard fix lands inState.HeardPosition/HeardAt/HeardSourcefor your loop’s investigate logic; a fresh sound resets the search scratch fields. Bus:Npc.Heard(npc, heardPosition, source?) - Pathfinding: terrain- and height-aware grids — the default rasterizer now raycasts for ground (Terrain, parts, and model geometry all count) and requires the agent column above it clear; cells carry their ground Y, so waypoints follow elevation, adjacent rises past
MaxStepHeight(default 4) read as unclimbable cliffs in A*/Theta* expansion, and grid line of sight refuses to cross walkable high ground — a wall top still occludes even though you could stand on it. InjectedIsWalkablemay return a second ground-Y value to opt in; plain boolean rasterizers behave exactly as before - NPCKit: server-side npc handles — archetypes carry a skill model with seven difficulty presets (
Perfectaimbot throughBad, or any custom{AimErrorDegrees, ReactionSeconds, LeadSkill, CooldownMultiplier}table, overridable per spawn) driving aim spray, reaction delay, moving-target lead, and attack pace, and movesets UNIFIED with player systems:Projectileactions fire the same kernel Projectiles definitions players shoot (npc handle rides as the owner session),Customactions receive a pre-aimed direction for pointing at existing WeaponKit/SpellKit server logic,Meleegates on range for creature attacks. Your game’s loop drives the handles (kit:all(),npc:updateTarget,npc:act,npc:moveTowardsre-pathing through Pathfinding as goals move); spawned models auto-track in Zones (Zone.EntityEntered/Left). Bus:Npc.Spawned/Died/Action - Pathfinding: one handler, four methods, each lazily required on first use —
AStar(8-connected grid, octile heuristic, corner-cut safe),ThetaStar(any-angle: parents shortcut through line of sight for straight segments),Direct(single LoS test; grid raster or world raycast),Roblox(PathfindingService navmesh; yields). Grids rasterize a world rectangle lazily with caching (refresh()after map changes), take injectedIsWalkablefor RTS maps/dungeons/deterministic specs, and searches carry aMaxExpansionsbudget with reasoned failures (GoalBlocked,NoPath,BudgetExhausted) - QuestKit: quests as data — objectives count server-published bus topics (
{Topic, Count?, Match?, Filter?}), so there is no client input to validate. Default rule counts events whose args include the session/player;Matchpins an arg (zone name, weapon id);Filtertakes over for actor-less topics.AutoAssign,Repeatable(resets fresh on completion),OnCompleterewards, progress persisted via profile with transient/persisted merge on load. Bus:Quest.Progress(player, questId, index, count, required),Quest.Completed(player, questId). See QuestKit - InteractionKit: exploit-proof ProximityPrompts — prompts are created from CollectionService tags (and follow tag changes live), but a trigger is client input, so every one re-validates distance (latency slack), server-side line of sight, and per-player cooldown through the fail-closed
Intent.Interactchain (kit gate at 50; game rules prepend) beforeOnInteractruns. Bus:Interact.Triggered(session, id, instance),Interact.Rejected(player, id). See InteractionKit - Settings: persisted player preferences behind an allowlisted schema — client writes ride a rate-limited intent through the fail-closed chain where unknown keys reject, numbers clamp, strings cap, and string arrays REBUILD clean so hidden hash keys never reach storage; values persist via the DataDriver profile (player-chosen transient values win the merge),
Settings_Syncpushes authoritative values back, andSettingsClientgives the client fetch/optimistic-set/onChanged over the same channels (wire to InputDriverrebindfor persistent remaps). Bus:Settings.Changed(session, key, value). See Player settings - Analytics: batched, sampled, rate-capped event pipeline —
track()is a queue insert, a background flush hands batches toAnalyticsService:LogCustomEvent(funnels/retention with zero infrastructure) or an injected destination (webhook/warehouse); per-event sampling and a per-minute cap protect the custom-event budget with every drop counted, never silent; optionalWatchErrorsbridgesKernel.ScriptError. Crashing destinations lose only their own batch. See Analytics Kernel:securityAudit(): one pre-ship sweep over the whole security model — flags handler-bearing channels with no validator (High), gate hook points flippedFailOpenwhere a crashing validator would PASS payloads (High),Open = trueescape hatches to re-confirm against hostile input (Medium), and channels still on the default rate limit (Info); prints a severity-sorted summary and returns findings for CI-styleassert(#findings == 0)gates.HookRegistrygainedlistPoints(). See Fuzz & audit- Zone-scoped replica interest:
replica:bindZone(zones, zoneName)— subscription flips the instantZone.Entered/Leftfires (full snapshot on entry, remove on exit, quantize baselines flushed) instead of waiting for the replica’s interest scan, which stays wired underneath viaisInsideso pre-bind occupants converge. Players outside a region pay zero bytes for its replicas. See Replica - Boot readout floors at 0.01ms — a sub-hundredth-millisecond boot no longer prints a suspicious “0ms”
- Zones: entity tracking, per-zone signals, multi-part volumes, tag wiring — zones now detect NPCs/props/vehicles alongside players:
track(entity)/untrack(returns an untrack fn) andtrackTag(tag)(CollectionService, live; untag or Destroy fires the leaves) put entities in the sweep, riding newZone.EntityEntered/EntityLeftbus topics whileZone.Entered/Leftstay players-only.add()now returns a per-zone handle carryingEntered/LeftSignals plusOnEnter/OnLeavecallback options, so handlers stop name-filtering the global topics; session-end andremove()leaves flow through the same dispatch (handles fire everywhere). One zone can span several parts (add(name, { PartA, PartB }),addPart), andaddTagged(tag)builds zones straight from Studio tags — zone name from theZoneNameattribute (fallback: part Name), same-named parts unioning into one zone. New reads:entitiesIn(name),isInside(name, occupant). InjectedQuerydoubles and the default overlap-query reuse are unchanged. See Zones - Built-in hook point & bus topic catalog — a new API-reference section documenting every hook point the framework fires (context fields, fail-open vs fail-closed vs observational, when it runs) and every bus topic it publishes (args, publisher), plus the consumed
AntiExploit.Forgivepardon topic and the hooks-vs-bus mental model (hooks ask permission, topics announce facts). Zones cookbook and API rows rewritten for the new surface — on this site: Hook Points & Bus Topics - ProceduralKit: server-authoritative runtime geometry on Roblox ProceduralModels (Studio beta) — archetype registry with fail-closed parameter validation (unknown names/wrong types reject, numbers clamp to Min/Max, OneOf enforced), per-model regeneration rate limiting (writes inside the cooldown buffer and flush as ONE batched rebuild — a spammed customization intent costs one regen per window, not one per message), session-bound lifecycle (
Owneroption),waitForGenerationsurfacingGenerationError, and a publicvalidate()for wiring client intents through the kit’s rules. Generators stay server-side by design: the peer that changes a parameter generates and the results replicate, so ServerScriptService generators are never decompilable. Ships an ExampleGenerator demonstratingparams:Pause()timeout safety and Seed-derived determinism. Bus:Procedural.Spawned/Updated/Rejected/Removed. See ProceduralKit - Engine diagnostics that survive production: new
Diag{Role}ReplKbpsIn/Out,PhysKbpsIn/Out,Primitives,MovingPrimitives,Contactsattributes plus a panel Engine section (replication bandwidth, physics replication, awake primitives, contacts) — none gated by memory tracking, so live servers keep a full engine debugging surface. Investigated enabling engine memory tracking by default: impossible — Roblox turns it off in all production builds before any code runs andStats.MemoryTrackingEnabledis READ-ONLY with no opt-in (staff-confirmed); ALL memory getters (incl.GetTotalMemoryUsageMb) return 0 + warn when off. The panel now states this plainly where the memory bars would be instead of silently omitting rows - Frame-aware scheduler budgeting (server default): each step budgets the slack left under a 16.5ms frame target after everything else’s measured share of the frame —
Stats.HeartbeatTimeMs(the engine’s smoothed heartbeat-phase compute, idle excluded, verified empirically) plusStats.PhysicsStepTimeMs, with the kernel’s own smoothed share subtracted back out so its own work never shrinks its own budget. An idle server drains ~16ms of backlog per frame; an engine-heavy one backs off automatically, never below the 1.5msBudgetSecondsfloor; a frame-interval guard drops to the floor when frames run >25% late (covers unmeasured costs like replication serialization, and misreporting platforms). ExplicitBudgetSecondskeeps the old fixed-slice behavior;TargetFrameSecondsopts any scheduler in; clients default to fixed 1.5ms (the render thread is invisible to the frame stat); phase schedulers stay fixed so a second frame-aware scheduler cannot double-claim the same slack. See Scheduler - Frame composition diagnostics — the frame decomposed by WHO is spending it:
Scheduler.Statsrecords the live budget, heartbeat-phase busy time, and physics time every step, plus a windowed step avg/max/deferred drained bytakeStepWindow(); newDiag{Role}FrameBusyMs/PhysicsMs/FrameTargetMs/StepMaxMsattributes; both panel windows grew a Frame section splitting measured compute into engine + other scripts / physics / kernel share (client adds render CPU) with a frame-compute bar against the target and per-row diagnosis tooltips Overloadsimulation mode: ~20ms of scheduled burn demand per frame (8 tasks every step on a Low/Background mix) plus 500 entities — deliberately exceeds the whole frame target so the budget pegs,Deferredgoes nonzero, queues back up, and the red panel states show (verified live: 74-143 deferrals/window, frame compute ~18ms). HighIntensity reframed as the realistic load a modern server clears green in ~1.2ms/frame — that being visible is the point. See Simulation- Simulation harness (
Simulation.server.luau+ChloeKernelServer/Simulation): three load modes selected by theChloeKernelSimulationReplicatedStorage attribute —HighIntensity(shooter-scale stress: 200 entity processes, hook/bus floods, replica delta churn, CPU burn),SmallGame(light baseline),Showcase(narrated 6-act feature tour incl. real haste/slow on present players and an anti-dupe replay demo). Hot-swappable mid-game, full teardown, Studio-gated, fake packet factories so simulated traffic never touches the real wire. See Simulation - GcWatch: GC statistics from
gcinfo()sampling — allocation rate (the lever that drives Luau’s incremental GC cost; Roblox exposes no pause timings), GC cycle cadence, and reclaim sizes that tell leaks (heap climbs, reclaims ~0) apart from churn (heap stable, busy cycles). Auto-attached byenableDiagnostics(newDiag{Role}GcAllocKbS/GcCyclesMin/GcReclaimKbattributes), graded in both debug-panel windows, and reported in the simulation heartbeat. See GcWatch & ErrorWatch - SimPanel + SimLoad (
Debug/): a SIM CONTROL window that switches server simulation modes live (Studio-onlyChloeKernelSimControlremote, mode-whitelisted) and runs client-side loads on the local machine — SimLoad is the role-agnostic rig set built on shared primitives only (processes, hooks, bus, serde, pools, burn) with its own GC-reporting heartbeat. Wired through the test place’s client Bootstrap; delete that block for a real game. See Debug panels - DataDriver backups (
Config.Backup): rotated periodic copies to any second backend (DataStore{Name}_Backupsby default; Memory/Http/custom for external DBs), optional per-saveMirrorto a live slot, and load-time fallback that restores the newest readable backup when a record is corrupt or unmigratable (profile.RestoredFromBackup+Kernel.ProfileRestoredbus event). Exactly-once per key per interval game-wide with zero coordination — backups ride the session-lock owner’s save path and the schedule lives inside the record, so no server count can duplicate them and server hops don’t reset the clock. Restore never bypasses session locks (corrupt ≠ unlocked) and never runs while the backend is down. New APIs:backupNow,listBackups,peekBackup(read-only, lock-free);Config.Clockinjectable for tests. See DataDriver - SOLID untyped-value support:
"Any"is a first-class Serde schema type (adaptive best-fit per value, clear errors for unstorable kinds);Serde.schema(spec, { Extras = true })preserves fields absent from the schema adaptively instead of silently dropping them;Serde.infer(sample)builds a typed spec from plain data with safe wide widths (integers → U32/S32, floats → F64 — never narrowed from a sample’s magnitude); DataDriver acceptsCodec = "Auto"(inferred from Defaults, extras on). See Serde & Base64 - Debug panel NET window + NetTap: a live wire inspector for kernel channel traffic from both machines — direction, channel, decoded args, payload bytes computed from schema widths, server processing ms / request round-trip ms, and verdicts (OK/Rejected/RateLimited/NoSession/Dropped incl. Packet flood drops and oversized unreliable sends). Capture is pausable (freeze the tail), clearable, and filterable (replica noise toggle); clicking a row pins it in an Inspector that shows the message both ways — decoded argument values AND the serialized payload as hex, generated through the same Packet type engine that writes the wire. Server entries reach the client on a debug-only mirror channel that never exists in production. SimPanel’s control path is now a real kernel intent, so mode clicks demonstrate the full pipeline in the window. ImGui gained a
selectablerow widget. See Debug panels - Panel limit bars (consumed vs permitted): both scheduler sections show a frame-budget bar (
used / budget ms (%)— 100% is where tasks start deferring; budget published via newDiag{Role}BudgetMsattribute) and the server memory row reads against Roblox’s 6.25GB process cap; ImGui bars now support hover hints - Replica
Quantizedeadbands ({[field] = threshold}): writes that moved less than the threshold from the last replicated value update server data but skip the wire entirely — drift is bounded by the threshold, so slow movement still replicates once it accumulates. Sub-visible jitter on chatty fields (positions, progress bars) now costs zero bytes. The NET window’s replica rows also show per-recipient size (21B each x30) so fan-out totals don’t read as one large buffer - Packet ported into the kernel (
ChloeKernel/Net/Packet): no more external package — the wire transport is now kernel-maintained, formatted to kernel standards, and bug-fixable in place. Attribution to Suphi Kaner (5uphi) retained in CREDITS.md; thePackages/folder is gone and all requires point inside the kernel. See Packet & wire types - Serde fast engine (techniques from light/holy by @hardlyardi, MIT): schema and struct codecs now compile per-type writers that resolve byte widths at compile time, allocate once at the exact size, and run with zero capacity checks and no global-cursor swap — fixed-schema encode 463ns to 285ns (38% faster), decode 514ns to 367ns, dynamic schemas 22-27% faster, replication deltas at 270/293ns. Output is byte-identical to the wire-engine path (spec-asserted with cross-decoding); schemas using uncovered types (Any, CFrames, sequences, Extras) fall back automatically, so no stored record or wire format changes
NumberVlqwire type: variable-length unsigned integer (7-bit continuation, up to 2^53) — 1 byte under 128, 2 under 16384; usable in schemas, structs, and channels; clamps like the other integer types- selene std extension (
buffer_bitops.yml) teaching selene 0.28 aboutbuffer.readbits/writebitsinstead of file-level allowances - BusBridge fulfills the Bus’s advertised NetBridge:
ServerTopics(wildcards) auto-forward to every client as ordinary bus publishes,Bus:publishRemoteforwards any topic explicitly, and client publishes ride a rate-limited intent through exact-name whitelisting (fail-closed, no wildcards upward) plus theIntent.BusPublishhook chain, arriving as(topic, session, ...). See BusBridge - MessageDriver: cross-server events over MessagingService — Serde-packed + base64 payloads, a clear error at the ~1KB ceiling, publish retries with backoff, per-message crash isolation for subscribers. Completes the trio: MemoryDriver = state, DataDriver = storage, MessageDriver = events. See MessageDriver
- Net Registry: single-source channel definitions consumed by both sides (
Registry.define/server/client) — schemas can never drift; declarativeValidaterules (Range/OneOf/MaxLength) compile into the fail-closed hook chains at priority 5;Net:onRequestadded for late handler attachment. See Registry - Server log tail in the panel: server Logger lines mirror to the SERVER window on a debug-only channel (same gate/pattern as the NET mirror), giving both windows a live tail
- SimLoad real-wire flood: client load modes fire the SimNoise kernel intent at rate (HighIntensity deliberately exceeds its 50/s limit so RateLimited verdicts demonstrate live in the NET window); NET byte estimates now cover
NumberVlq Kernel:shutdown(): full teardown — diagnostics, GcWatch, services, phase schedulers, the main scheduler, and the bus — and clears the boot singleton, so a freshboot()on the same modules works (tests, in-place re-runs)ChloeKernelDebugUserIdsallowlist: comma-separated UserIds restricting the live-server debug surface per user — panel attach, the NET/log mirrors (now per-recipient sends, neverFireAllClients), and the SimControl intent- Teardown across the board: ProjectileClient
destroy()(+ client-side safety TTL and serial-wrap recycling), ReplicaClientunlisten()/destroy(), ImGuiWindow:destroy()(drag connection severed), Panel/NetPanel detach handles, Effectsdetach(), Zonesdestroy(), ErrorWatchdestroy(), Matchmaking destroy now unsubscribes everything - LiveConfig override deletion:
set(key, nil)removes the key from the shared document and every server reverts to its default on the next poll (previously a removed override stayed stale until restart). See LiveConfig - Player records persist
Meta.UserIds, forwarded by DataStoreBackend toUpdateAsyncas the metadata Roblox uses for GDPR erasure tracking - CI: rojo build smoke test and luau-lsp strict type analysis (non-blocking until the
(self :: any)escape hatches are typed away); lint paths widened to all ofsrc; leftoverHello.luauscaffolding removed - InputDriver multi-key bindings:
Keyboard/Gamepadaccept oneEnum.KeyCodeor an ARRAY of them, so one action can ride several physical inputs;rebindandgetBindingssnapshots mirror the shape. See InputDriver - Typed public surface for Studio autocomplete:
ServerKernelfields carry concrete types (Scheduler/Hooks/Bus/Services/NetDriver instead ofany),kernel:net()returns a typedNetDriver(the module loads at boot purely for its type — driver CONSTRUCTION stays lazy),session.Profileis typed asData.Profile, andregisterService/spawnProcessparameters are annotated - Intents/requests are now fail-closed on a MISSING validator, not just on a failing one. A channel with a
Handlerbut zero validators rejects every payload unless explicitly marked{ Open = true }— closing the footgun where an unguardedGrantCoins/SellItemintent let a client mint currency or set a stat at will (an absent validator used to mean “accept anything”). DeclaringValidaterules counts as a validator;Open = trueis the audited escape hatch for genuinely no-auth channels (payload-less toggles, idempotent ready-ups). Surfaced three ways: a one-time[NetDriver] ... fail-closedwarn naming the channel, anUnguardedverdict in the NET panel, andNet:auditValidators()for a proactive post-boot sweep.Registrychannel defs gainedOpentoo. Existing kit/Registry channels are unaffected (they all register validators); only handler-bearing channels you never guarded change behavior — in the safe direction, visibly, in dev - The whole tree type-checks clean (1116 → 0 luau-lsp diagnostics) and the CI analysis step is now BLOCKING: Studio’s script analysis shows zero squiggles across the framework. Fixes were annotation-only — named option-type locals for the
options or {}union trap,ImGui.Ui-typed render callbacks,:: { any }on the deliberately heterogeneous Static/Enums templates, expression-level casts where the old solver loses refinements. Spec files moved to--!nonstrict(their fakes are deliberately partial doubles; the framework itself stays strict). luau-lsp pin corrected to a real release (1.68.0) - NET inspector shows per-argument wire types: pinning a row now lists each argument’s wire type, and
"Any"arguments expand to the adaptive engine’s actual best-fit pick (Any:U8,Any:F32,Any:Table{Wave:U8}, …) — verify at a glance that values ride the optimal width. Mirrored server entries carry it too (NetTap.describeWireis public) - Rewind’s
MaxFutureSecondsdefault raised 50ms → 200ms: realGetServerTimeNowskew plus frame timing on rough connections could push honest stamps past 50ms and reject asFutureTimestamp; a future stamp only clamps to the newest sample, so the looser default costs nothing. See Rewind - Live servers no longer flood the console with “Memory tracking is currently disabled” warnings: the panel’s Total/Script-memory rows and the diagnostics MemoryMb attribute now check
Stats.MemoryTrackingEnabledfirst (live servers run with tracking off; Studio always tracks) — the panel shows a dim explanatory row instead and the SERVER window simply omits the bar - Hitbox visualization (
ShowHitboxesattach option /lag:showHitboxes(enabled)): everycastRaydraws the rewound capsules it tested — green for the hit, red for tested-and-missed — as short-lived server parts every client sees. Visual only, zero cost when off.Rewind.drawHitboxis public for game tooling - Projectiles capsule mode: setting
Hitbox = {Radius?, HalfHeight?}on a projectile definition switches PLAYER hit resolution from raw character-geometry raycasts to the same capsule hitboxes the hitscan path uses (consistent per-weapon forgiveness knobs), and the landing step draws the tested capsules through the attached Rewind’sShowHitboxesdisplay. Definitions withoutHitboxkeep the legacy behavior exactly; in capsule modeOnHitreceives{ Position, Instance }instead of an engine RaycastResult - Lag-compensated hit DETECTION with per-weapon hitboxes:
Rewind:castRay(shooter, origin, direction, range, timestamp, hitbox?)casts a shot against every tracked player’s REWOUND capsule (pureRewind.rayCapsuleunderneath, spec-verified) — previously only the CLAIM was lag-compensated while the actual raycast ran at live positions, so high-ping shots missed strafing targets the shooter had dead-on. WeaponKit uses it wheneverconfig.Rewindis attached, with per-weaponHitbox = { Radius?, HalfHeight? }sizes (default 1.6/2.4); world geometry still blocks at its current state — walls don’t rewind, and live character bodies never block shots aimed at where targets USED to be
Security
Section titled “Security”- Decode paths hardened against hostile bytes (wire and storage): length prefixes are bounds-checked against the remaining payload BEFORE any allocation — a 5-byte payload with a crafted U32 prefix could previously force a multi-GB
buffer.createon the server. Adaptive table decode is depth-capped (48), unknown packet/adaptive-type/enum/static ids error with clear messages instead of nil-indexing, VLQ reads cap at 8 continuation bytes, and the client receive loop is pcall-isolated like the server’s - BusBridge reflection closed: a topic whitelisted in
ClientTopicsthat also matched aServerTopicswildcard reflected the serverSessionobject (plus attacker-chosen args) to every client; client-originated publishes never auto-forward down anymore
Fixed (data safety)
Section titled “Fixed (data safety)”- HttpBackend treated 404 as success on every method — a misconfigured BaseUrl or changed route made every save report OK while writing nothing; 404 now only means “new key” on GET, and PUTs carry
If-Matchwhen the API returns an ETag so a concurrent writer loses the CAS instead of last-write-wins - A player leaving during a slow profile load orphaned the profile forever (the session teardown had already run,
bind()threw, autosave kept the lock fresh, and the player was kicked on every rejoin until the server died); the load now releases immediately when the session ended mid-flight - Autosave ran inline in the scheduler step, stalling every other kernel task (projectiles, sweeps, zones) for the duration of DataStore round-trips including retry ladders; it now runs off-thread, paces saves across a quarter of the interval, and never overlaps itself.
DataDriver.newassertsLockTtlSeconds >= 2x AutosaveSeconds— the autosave is the lock heartbeat - One in-flight save per profile: autosave,
save(), andrelease()could interleave retry ladders, landing an older snapshot after a newer one or re-locking a key the release just unlocked; release deactivation now happens inside the mutex - A failed final save was dropped silently; it now retries in the background (5/15/30s) before giving up loudly, and
releaseAllbails before BindToClose’s 30s hard kill with a warn naming every unsaved key - Schema downgrades refuse to load: a rolled-back deploy no longer runs old code over newer-schema records — and the refusal is non-recoverable, so it can never trigger a backup restore over newer data
- Receipts could acknowledge an unsaved grant: a re-invocation during the save window hit the ledger check and returned
PurchaseGrantedwith zero persistence (a crash then lost a paid grant); acknowledgement now requires a landed save on every path, grant handlers roll back partial mutations on error, and the dedupe ledger default grew 50 → 200 - Transactions: replay ids persist in
Data— written atomically with the trade itself — so DuplicateTransaction protection survives rejoin/server hop; a throwingsave()can no longer leak both InFlight locks findUnserializablerejects sparse arrays and non-integer numeric keys (they do not survive a JSON round-trip); MemoryDriver’s buffer probe no longer condemns the driver to base64 on a transient error, andgetRangereports a corrupt entry instead of throwing
Fixed (kernel + networking)
Section titled “Fixed (kernel + networking)”- Process suspend/resume race double-stepped forever: resuming before the queued step drained scheduled a second copy and both chains re-enqueued themselves; a
StepQueuedflag keeps exactly one chain. Self-kill no longer abandons the suspended thread unclosed or double-finishes as Completed; the PID registry is weak-valued so dropped processes collect - Scheduler recurring tasks no longer backlog under budget starvation — a starved 0.1s task could queue ~10 copies that ran back-to-back the moment budget freed, exactly the burst the snap-forward comment claimed to prevent
- Predicted-intent acks ride AFTER the handler with error isolation: “accepted” now means “applied” — a throwing handler acks a rollback instead of leaving the client’s optimistic effect diverged from a server that changed nothing. Request handlers pcall to
RejectValue, and a timed-outinvokeresumes withRejectValueinstead of nil (one failure encoding) - Rate-limit buckets keyed to departed Players could be recreated after session cleanup (Packet dispatches deferred); the session check now precedes bucket creation
Registry.predict()builds one handle per channel per client — repeated calls previously shared the ack channel while running separate sequence spaces, so an ack for one handle resolved (or rolled back) the wrong handle’s entry- MessageDriver’s 1KB guard measures the base64 payload — capping raw bytes passed 769-950-byte messages the service then deterministically rejected after burning the whole retry ladder
- Replica interest joins converge every view: pending sub-threshold quantize drift flushes to existing subscribers when a snapshot is taken (the documented “threshold is the maximum drift” bound previously degraded to ~2x for joiners), same-tick joiners skip the duplicate delta, and no-op scalar
set()s stay off the wire entirely - HookRegistry fires over a snapshot — a handler unregistering itself mid-fire shifted the live array and silently SKIPPED the next handler; in a fail-closed validation chain that is the wrong failure mode
- ErrorWatch requires Logger relatively (a cloned kernel silently shared the original’s Logger), prunes its dedup table (interpolated error messages each minted a permanent key), and stores its connections
Signal:destroy()resumeswait()-parked threads instead of leaking them suspended forever;Pool:release()errors on a double release (two acquires could share one Instance);expect().toEqualsurvives cyclic tables; a throwing InputDriver handler no longer suppresses the action’s Bus publish; Bus stops caching matchless dynamic topics and clears its wildcard cache on destroy; Logger sink dispatch reuses one thread instead of allocating a closure + coroutine per log line per sink
Fixed (serialization)
Section titled “Fixed (serialization)”String/Bufferlength prefixes are enforced in both engines — a 300-byte value previously wrapped its U8 prefix to 44 and silently desynced the whole stream (documented as “max 255”, never checked)- Adaptive fractional numbers take the F32 form only when it round-trips exactly —
decode(encode(0.1))now returns 0.1 (F64), keeping the “smallest width that holds the value” promise honest Static1/2/3error on unregistered values both ways instead of writing index 0 and decoding to a silent nil;EnumItemerrors clearly on unknown types/values;UDim/UDim2Scale clamps at the S16 field’s range instead of overflowing; the Packet factory errors past 255 channels (U8 wire ids would silently collide); the shared-cursor no-yield invariant is documented at both engines
Fixed (gameplay)
Section titled “Fixed (gameplay)”- Matchmaking dequeues tombstone their queue entries (MemoryStore queues can’t remove by value): coordinators on every server filter reads against the tombstone map, consume poisoned reads, and re-queue live members — no more undersized matches built around ghosts. Match teleports retry with backoff and re-queue stranded members, publishing
Matchmaking.TeleportFailed - SoftShutdown covered no reserved servers: Matchmaking match servers matched the transit-server check (reserved, owner 0) and got NO BindToClose protection; the bounce decision is per-player join data now, every reserved server gets shutdown protection, and bounce/transit teleports retry
- Movement’s teleport threshold prorates with the sample gap — a server hitch no longer reads a fast-but-legit runner crossing 80 studs over several missed sweeps as a teleport; rubberbands preserve the character’s facing
- Rewind buffers reset on respawn and on
AntiExploit.Forgive— a buffer spanning death → spawn interpolated between the two and rejected honest post-respawn shots as OriginMismatch. WeaponKit anchors the muzzle to the shooter’s REWOUND position (ping x move speed no longer eats the 8-stud origin tolerance) and consumes the fire-rate window only when the whole validator chain passes - CheckpointKit no longer races the async profile load: the stage field initializes lazily per touch instead of eagerly at join (where it pinned session data and crashed the validator once the profile attached without the field); progress earned during the load window merges into the profile on
Kernel.ProfileLoaded - Effects state is per-instance — the Simulation showcase attached a second Effects to the real kernel and its sweep removed (and its recompute clobbered) the game’s own effects; ActorPool surfaces worker require failures (
CKDeadattribute + clear dispatch error) instead of burning a full timeout per dispatch forever, and guards dispatch-after-destroy
Fixed (ImGui)
Section titled “Fixed (ImGui)”- Stale-click delivery across relabeled buttons: widget slots are reused by call order, so a dynamic button list could hand a pending click to a DIFFERENT logical button on the next render (mode buttons relabel exactly like this). Buttons now drop pending clicks when their label changes
Performance
Section titled “Performance”- Projectiles capsule mode hoists per-tick work out of the projectile loop: one world-pass exclude list and one alive-target snapshot per step, shared by every projectile (was a rebuild plus a
FindFirstChildsweep per player per projectile per tick), and the default raycast reuses a singleRaycastParams, reassigningFilterDescendantsInstancesonly when the list identity changes - Process steps stopped allocating a queue entry + handle per resume: new
Scheduler:scheduleEntryre-enqueues one caller-owned entry per process for its whole life (the handle was never used). A/B under HighIntensity: GC alloc 4.4MB/s -> 0.6MB/s (7x) with step times statistically identical Bus:publishno longer allocates on unmatched topics (shared frozen empty match) and the wildcard match cache is bounded at 512 entries (reset-and-repopulate past the cap), so dynamic per-entity topics that match a broad wildcard cannot pin one cache entry each forever- Replicas of the same tick rate share ONE recurring scheduler entry — the scheduler walks every recurring entry every frame, so a hundred 10Hz replicas now cost one slot in that scan, not a hundred
- Rewind samples cache the HumanoidRootPart per character (
FindFirstChildon spawn/respawn, not SampleHz times a second per player); Zones sweeps build one character include list per sweep with one reusedOverlapParams;Logger.emitearly-outs before building the entry when the console is off and no sinks exist - Process stepping no longer allocates a closure per resume. Every yield re-scheduled via a fresh anonymous closure, which missed the profiler’s function-identity cache on every step (full
debug.info+ string cost, the same class the 0.2.0 profiler fix addressed) and fed the GC a closure per process per frame. One stable step closure per process now: under the HighIntensity simulation (200 entities + bolts) server step time dropped 1.33ms → 0.50ms and GC alloc fell ~20% (4.9 → 3.9MB/s) - Hot tasks attribute process time to the process, not the kernel. New
Scheduler.setTaskOrigin(fn, label); Process registers each step closure asProcess <name>, so the panel now readsProcess SimBolt 19.8ms/sinstead of lumping everything intoProcess:89— which also revealed the real cost driver in the sim (boltPart.CFramewrites, 3× the cost of 200 pure-math entities)
Changed
Section titled “Changed”- License: private access. ChloeKernel access is granted individually by Chloe and is revocable; granted users may not share the source or grant access on her behalf. Game-use grants, required CryptedChloes attribution, the no-redistribution rule, and the vendored Packet exclusion are unchanged. Public docs live in ChloeKernel-Docs; the docs being public grants no framework access
- README rewritten in reference-documentation tone: introduction/pitch phrasing removed, stale spec counts fixed, and examples reworked to demonstrate features per config field (neutral names, annotated gates) instead of telling game stories — the intent example now shows schema/rate-limit/validator-chain/handler explicitly, and the kit examples document what each field enforces
- Deferred was invisible during overload:
TasksDeferredwas reset at step start and published by a task running INSIDE a step, so it structurally read zero exactly when overload happened (and under hard saturation the Background-priority diagnostics task stopped running entirely). Counts now finalize at step end (readers always see the last completed step) and SUM into the diagnostics window; the diagnostics publisher andKernel.Overloadstreak moved to Kernel priority — health reporting must survive the very overload that starves Background work - Server step diagnostics no longer phase-lock:
Diag{Role}StepMspublished a single last-step point sample; at a 1s diagnostics cadence against 0.1s recurring work the sample aligned with the same frame type for a whole session (reading ~35% of budget one boot and ~180% the next with nothing changed). Now a windowed average plus the newStepMaxMs - Vendored Packet (marked mod #5): the server flush loop is pcall-isolated — a
FireClientthat throws (a player disconnecting inside the PlayerRemoving cleanup window) killed the while-true flush thread and silently stopped EVERY outgoing packet for the rest of the server’s life - Effects/Zones sweeps collect first, fire after: expiry removes and enter/leave publishes ran synchronously mid-iteration, so a subscriber that re-applied an effect (or added/removed zones) inserted keys into the very tables being iterated — undefined behavior in Luau iteration
- Transactions commit in place, preserving table identity:
profile.Datawas swapped to a new table on commit, silently orphaning any reference game code held to it (or any cached subtable); commits are now deep in-place merges, so every identity survives the trade, and rollback restores from a pre-commit deep backup - Prediction acks
task.canceltheir timeout thread instead of letting it idle out the window; ReplicaClient pending never-listened replicas are capped at 64 with oldest-first eviction and a warn - Logger/ErrorWatch specs disabled the Logger console sink and never restored it, silencing ALL console logging for the rest of any Studio session that ran the boot suite (found by the simulation harness’s missing heartbeat)
- Vendored Packet (marked mod #3): the
Anyengine’s string/buffer writes used a U8 length prefix, so values over 255 bytes wrapped the length byte and silently corrupted the stream. Long values now use new type ids 29/30 with a U32 length; short values keep the original framing (stock payloads decode unchanged) - Packet F16/F24 halved values at power-of-two boundaries (mod #4): the writers put the rounded mantissa straight into the bit field, so any value whose mantissa rounded up to a power of two (63.99, 127.99, 255.99 — observed as health values collapsing:
63.99 -> 32) overflowed the field to 0 with an unbumped exponent and decoded at HALF its value;Vector3F24components were equally affected. The carry now bumps the exponent (63.99 encodes as 64), clamping at each format’s max. Sub-normal magnitudes (below 2^-14 F16 / 2^-30 F24), which previously wrote a negative exponent field, flush to zero
0.2.0 — 2026-06-09
Section titled “0.2.0 — 2026-06-09”The gameplay-and-tooling release. Everything verified live in Studio play sessions; 196 specs at every boot.
- Replication: ReplicaService/ReplicaClient — interest management, per-replica tick rates, field-indexed deltas (
Serde.struct). See Replica - Gameplay: RoundKit (phase state machine), Projectiles (server-simulated, lag-comp validated, pooled client visuals), Effects (stacking buffs/debuffs with a generic stat pipeline), Prediction (instant client actions with server ack/rollback)
- Networking: unreliable state channels, predicted intents, configurable Packet rate limit with flood reporting
- Utilities: Pool, Zones (leave-before-enter region events), Leaderstats, ErrorWatch (deduplicated error telemetry, now covering kernel task crashes), LiveConfig (MemoryStore-backed feature flags)
- Scheduler: engine-phase schedulers (PreSimulation/PostSimulation/PreRender), drift-free recurring tasks, per-task hot-origin profiler (~33ns/task) with MicroProfiler zones (Studio default),
queueDepths()/topTasks()introspection - Serde: auto-coercion (round/clamp/parse what’s castable, fail fast on garbage), struct delta codecs
- Debug panel (F8, Studio-only): separate CLIENT/SERVER ImGui windows, health-graded stats, diagnostic hover tooltips, hot-task pinpointing by script:line, copyable stat dumps. See Debug panels
- PerfGuard spec: order-of-magnitude perf regressions now fail the boot suite
Changed
Section titled “Changed”- Framework renamed ChloKernel → ChloeKernel
- Demo removed from the repo; Bootstraps are clean templates
- Network-ownership guarantee documented: the kernel never reassigns character ownership
- Profiler overhead regression (2848ns → 354ns/task)
- ReplicaClient unbounded pending-delta buffering
- Rewind clock incompatibility with client timestamps (
GetServerTimeNow) - Projectile time dilation under deferred sim ticks
task.wait(0)frame-cost in retry backoffs
0.1.0 — 2026-06-09
Section titled “0.1.0 — 2026-06-09”Initial release: microkernel (scheduler/processes/services/IPC/hooks), server kernel with sessions, DataDriver (session locks, migrations, codecs), MemoryDriver, Serde, NetDriver (intents/states/requests), anti-exploit (movement + lag-compensated rewind), commerce (exactly-once receipts, atomic trades), InputDriver, SoftShutdown, Matchmaking, ActorPool, TestKit + Bench, genre kits (Weapon/Spell/Checkpoint).