Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

File formats

ExtensionWhatText or binaryWritten bySource analogue
.kmapEditable map sourcetext (KeyValues)Chisel.vmf
.kbspCompiled mapbinary, lump directoryCleave / Umbra / Resonance / Radiance.bsp
.kprtPortal graphtextCleave.prt
.kleakLeak tracetextCleave.lin
.kwalkNPC walkmapbinaryCleave(Source has no equivalent)
.ktexTexturebinary, resource containerAlchemy.vtf
.kmatMaterial sourcetext (KeyValues)Alchemy, by hand.vmt
.kmat_cCompiled materialbinary, resource containerAlchemy.vmat_c
texture.kcfgTexture set definitiontext (KeyValues)Alchemy, by hand(no equivalent)
.kmdlModelbinary, resource containerForge.mdl
.kdefEntity class definitionstext (KeyValues)the game, by hand.fgd
.kscrLevel script, UI scripttext (Rhai)by hand.nut (VScript)
.kuiUI layouttext (XML)by handPanorama .xml
.kcssUI stylesheettext (CSS subset)by handPanorama .css
.ksndSound scripttext (KeyValues)by handgame_sounds.txt
.kaudCompiled soundbinaryTimbre.wav (ADPCM)
.wav .flac .mp3Sound sourcesbinaryany audio tool.wav
.vaultContent archivebinaryVault.vpk

Text where a person edits or reviews it; binary where the engine loads it. The runtime reads no source format: a .kmat becomes a .kmat_c in the build, as a .png becomes a .ktex.

Every coordinate in every one of them is in kerosene units – one kerosene unit is one inch, a player is 72 of them tall. Angles are degrees, times are seconds, and Z is up.

KeyValues

The text format .kmap, .kmat, .kdef and the compiled entity lump all use.

world
{
    "classname" "worldspawn"
    "skyname"   "sky_kero"
    solid
    {
        "id" "1"
        // faces follow
    }
}

Two properties drive the implementation. Keys repeat — a world holds many solid blocks — so entries are an ordered list, never a map; a HashMap would silently eat brushes. And order is meaningful, so round-tripping a map through the editor does not reshuffle it and produce a noisy diff.

Backslashes are literal, so materials\dev\grid survives intact. Only the escapes the writer emits (\", \\, \n, \t) are resolved on read.

.kmap — editable maps

versioninfo { "formatversion" "1" }
world
{
    "id" "1"
    "classname" "worldspawn"
    solid
    {
        "id" "2"
        side
        {
            "id" "3"
            "plane"    "(0 0 0) (0 64 0) (64 64 0)"
            "material" "dev/grid"
            "uaxis"    "[1 0 0 0] 0.25"
            "vaxis"    "[0 -1 0 0] 0.25"
        }
    }
}
entity { "id" "9" "classname" "info_player_start" "origin" "0 0 32" }

Brushes are planes, not vertices. A solid is the intersection of its faces’ half-spaces. That makes convexity structural — you cannot author a brush with a hole in it — and it is what makes CSG possible. The cost is that a solid’s actual polygons only exist once something computes them.

Each plane is written as three points, listed clockwise as seen from the front of the face, so the normal is (p0 - p1) × (p2 - p1). That looks like the wrong cross product and is not: every brush face ever written to a .map-lineage file depends on exactly this ordering.

Faces carry no UVs. They carry two texture axes — world vectors a point is projected onto — which is what makes texturing feel the way it does in a brush editor: drag a brush and the texture stays locked to world space.

Any object can carry any key. An entity is a bag of "key" "value" pairs and always was; a solid and a side are too. The keys the format defines (plane, material, the axes, and so on) become typed fields; every other pair round-trips untouched, so a key the game or a compiler pass invents later, or one a person types into Chisel’s key-value editor, is never silently dropped. Cleave reads two on a solid: "detail" "1" compiles the brush as detail without tying it to detail, and "section" "name" puts it in a streamed section by name. Brush and face keys never reach the compiled map; a brush that should mean something to the game is tied to an entity, as in Source.

The editor’s own blocks. What Chisel needs to remember that the game does not is kept out of the properties so it can never leak into the compiled entity lump. Every solid and entity may carry an editor block, and the root may carry visgroups, any number of group blocks, and a cordon:

visgroups
{
    visgroup
    {
        "name"    "Cave"
        "id"      "40"
        "color"   "90 200 120"
        "visible" "0"
        "stream"  "1"
        visgroup { "name" "Pool" "id" "41" "color" "120 170 255" }
    }
}
solid
{
    "id" "2"
    side { ... }
    editor { "visgroupid" "41" "groupid" "50" "color" "220 30 30" "comments" "back stairs" }
}
group  { "id" "50" }
cordon { "mins" "-512 -512 0" "maxs" "512 512 256" "active" "1" }

VisGroups nest, and hiding a parent hides its children. A visgroup with "stream" "1" is also a section: Cleave tags its brushes with it and the engine streams the section’s geometry in and out around the player (see .kbsp below). Groups are flat: objects that share a groupid select together. "visible" "0" in an object’s editor block is quick-hide. Every one of these blocks is omitted when it would say nothing, so a map that uses none of them is byte-for-byte the map it was before they existed, and a reader that predates them skips them. Visgroup and group ids share the map’s one id space with entities, solids and sides.

Meshes. Beside its solids, the world may hold polygon meshes – Source 2 Hammer’s kind of geometry: points and the faces between them, any shape, open or closed, concave if it likes.

mesh
{
    "id" "40"
    vertices
    {
        "v" "0 0 0"
        "v" "0 64 0"
        "v" "64 64 16"
        "v" "64 0 16"
    }
    face
    {
        "id" "41"
        "v" "0 1 2 3"
        "material" "dev/grid"
        "uaxis" "[1 0 0 0] 0.25"
        "vaxis" "[0 -1 0 0] 0.25"
        "lightmapscale" "16"
    }
    editor { "visgroupid" "41" }
}

One vertex a line, so moving one is a one-line diff. A face lists vertex indices clockwise seen from the front, the same winding a brush face’s plane points and a compiled face use. Axes and lightmap scale default the way a brush face’s do. A face that is not flat and convex is cut into triangles when it compiles. Meshes are detail: Cleave draws, lights and collides with them – each face backed by an invisible 8-unit collision slab, so every trace in the engine sees it – but never lets them split the tree, seal the map or block visibility. Block a space out in brushes; dress it in meshes. Only the world’s meshes compile for now; a brush entity carrying one is warned about and left out.

.kbsp — compiled maps

A header (KROS, a version, a 24-slot lump directory) followed by flat arrays of #[repr(C)] records. Every record is padding-free, so loading a lump is a bounds check and a cast rather than a parse.

LumpHolds
entitiesKeyValues text, one block per entity
planesPairs: index n^1 is always the inverse of n
vertices, edges, surfedgesShared vertex positions and the ring indirection
facesRenderable polygons, with lightmap extents
nodes, leavesThe BSP tree
leaffaces, leafbrushesWhat each leaf holds
modelsModel 0 is the world; 1..n are brush entities
brushes, brushsidesConvex collision volumes
texinfo, texdata, texdata_stringsMaterials and their projections
visibilityThe run-length-encoded PVS, and the PAS beside it
lightingBaked lightmap samples
acousticsOne record per room: how long sound lingers there, per band
acoustic_leafsOne u16 per leaf: which room it is in
sectionsThe streamed sections: a name and bounds for each; section 0 is the world
face_sections, brush_sectionsOne u16 per face and per brush: which section it is in

The surfedge indirection. A face’s vertices are reached through a run of surfedges, each a signed index into the edge lump — negative meaning “walk this edge backwards”. Two faces meeting at an edge therefore share one edge record and one pair of vertex positions, which is what keeps their seam watertight no matter how the arithmetic rounds.

Plane pairs. Planes always live at indices 2k and 2k+1, so flipping one is index ^ 1. A node stores one plane index and its two children implicitly use the plane and its inverse.

Lightmap samples are ColorRgbExp32: three bytes and a shared exponent. That is what lets a baked lightmap carry values well above 1.0 — a bright sky, a lamp against a wall — in four bytes instead of twelve, and it is why the lighting can be tone-mapped at runtime rather than clipped at bake time.

Rooms. The two acoustics lumps took the directory’s two spare slots, so adding them moved no version. The first is an ACST header — the four band frequencies, then how many rooms and paths follow — and an 80-byte record per room: decay time per band, the absorption it was derived from, mean free path, pre-delay, openness, wet level, diffusion, a bounding sphere, volume, flags and a leaf count. The second is one u16 per leaf naming its room, 0xFFFF for a leaf in none. An empty acoustics lump is a map Resonance has not run on, the same way an empty visibility lump is a map without vis; the engine plays it dry and says so.

Sections. Version 2 added four slots for the streamed sections and kept one spare. A section is a visgroup the designer marked as streamed; Cleave numbers them in tree order after the world and tags every world face and brush with its number. The engine keeps the tree, the entities and the traces whole and streams each section’s render mesh, lightmap atlas and rigid-body hulls as the player moves. A compile always writes the lumps – one section, every face in it, for a map with no streamed groups – and a version 1 file is refused with a message to recompile.

Cubemaps. The last spare slot, so adding it moved no version either: a file from before it has the lump empty and reads as a map without probes. A KCUB header – version, face size, probe count – then each probe’s origin, then every probe’s six faces of RGB9E5 texels (three 9-bit mantissas and a shared 5-bit exponent: linear HDR in four bytes, on the same scale as the lightmap, 1.0 being a white surface lit to full). Radiance writes it from the env_cubemap entities, after baking the lightmaps the probes photograph. The face order and orientation are kerosene_bsp::cubemaps::face_basis – Kerosene’s own, since the renderer picks faces itself rather than using a hardware cube map. The next lump needs a version bump.

Every index in the file is validated at load. A dangling one becomes an out-of-bounds read deep inside the renderer, where the cause is invisible.

.kprt — the portal graph

Written by Cleave, read by Umbra.

VPRT1
<cluster count>
<portal count>
<points> <cluster a> <cluster b> (x y z) (x y z) ...

The winding’s own plane normal points toward the first cluster listed. Only portals between two non-solid leaves appear: sight does not travel through rock, so a portal with a solid side is not a portal.

.kwalk — the NPC walkmap

Written by Cleave, read by whatever drives NPC navigation. It is the answer to “where can NPCs go”, precomputed once at build time the same way the PVS precomputes visibility: the runtime asks a cheap question – is this point on a walkable face, and what is that face’s rule – instead of re-deriving it from the BSP.

A little-endian binary: a KRWL magic, a version, a face count, then one record per face holding its rule, its polygon and its normal. Cleave builds it from the world’s flat walkable faces – a face whose normal points up and wears ordinary geometry – and folds in each face’s walkmap rule chosen in Chisel:

  • allow – walkable if flat. The default; a floor that says nothing is one.
  • deny – never part of the walkmap, however flat.
  • avoid – walkable, but flagged for NPCs to route around.
  • always – part of the walkmap even if the face is not flat, for ramps.

Brush entities are absent on purpose: a door moves, and a static walkmap that said a closed door was open would send an NPC through it.

Navigation is built on top of the faces: kerosene-walk’s NavGraph links faces that share an edge and A*-searches them into a list of waypoints, with avoid faces given a movement-cost penalty so paths bend around them when a detour is not much longer. The runtime asks for a path the way it asks whether a point is walkable — cheaply, against data the compiler already produced.

The resource container

Compiled materials, textures and models share one layout, KRES, from the kerosene-resource crate. The rest (.kaud, .kbsp, .kwalk) still have a header of their own each, and move into it one at a time. A .ktex or .kmdl from before the container still loads: its old layout is exactly what the container’s DATA block now holds.

OffsetSizeField
04KRES
44Container version, 1
84Kind: four bytes naming the payload, KMAT, KTEX, KMDL
124Kind version
168Source hash: 64-bit FNV-1a of the source it was compiled from
244Block count
284Reserved, 0
3212 × countBlock table: tag, offset, length

Then the blocks, each on a 16-byte boundary. Three tags mean the same in every kind: DATA is the payload; REFS lists, as virtual paths, the other resources this one needs, which a packager follows; EDIT records the compiler, its arguments, and each input with its hash, so a tool can tell a stale file. A reader skips tags it does not know. All integers are little-endian. alchemy info prints a compiled material’s header and blocks.

.ktex — textures

The resource container, kind KTEX. Its DATA block is a 48-byte header (dimensions, format, flags, average colour) then the mip chain, largest first. Uncompressed RGBA8, RGB8 or R8.

Mipmaps, the average colour Radiance needs for bounce lighting, and sampling intent are all resolved at build time. Doing them at load costs startup on every run, and doing them well is not something to redo per launch.

.kmat — materials

lit
{
    "$basetexture"   "dev/grid"
    "$bumpmap"       "dev/grid_normal"
    "$roughness"     "dev/grid_rough"
    "$selfillummask" "dev/grid_emissive"
    "$ao"            "dev/grid_ao"
    "$metalnessmap"  "dev/grid_metal"
    "$surfaceprop"   "concrete"
}

That is the source. Alchemy compiles each one to a .kmat_c beside it (kind KMAT): the shader is resolved and the text parsed at build time, and the textures it names go in the REFS block. The engine loads the .kmat_c. A project whose content predates compiled materials still runs from its .kmat files, with a warning for each, until its next build.

The block name is the shader — lit, unlit, sky, water, ui. A small closed set, because every one is a real code path; an open-ended string would just be a way to fail at draw time instead of load time.

Geometry references materials, never textures, so retexturing a level or making every metal surface reflective is one file change. Unknown parameters round-trip rather than being dropped: a game will invent keys the engine has never heard of.

The six texture parameters above are the ones the renderer samples. Every one but $basetexture is optional and absent by default, and a material that names none of them renders exactly as it would have before they existed — the shader branches on a per-material word saying which are real, so there is one pipeline rather than a variant per combination.

ParameterWhat it does
$basetextureThe colour. Sampled through an sRGB view.
$bumpmapTangent-space normals. The basis comes from the face’s texture projection, so it agrees with the UVs by construction.
$roughnessRed channel, 0 mirror to 1 matte. Drives the specular term; with no map (and no $roughnessfactor) a dielectric is fully rough and gets none.
$selfillummaskWhat the surface emits regardless of light reaching it. Added in linear HDR before tone-mapping, so it rolls off like a lit highlight.
$aoBaked occlusion, multiplied into the lighting. Darkens what the surface shadows itself, which the lightmap’s luxels are far too coarse to see.
$metalnessmapRed channel, 0 dielectric to 1 bare metal. Metal has no diffuse colour; its base colour tints its reflection instead.

Two scalars go with the maps, the way glTF’s factors do: each multiplies its map, and with no map it is the whole answer.

ParameterDefaultWhat it does
$metalness0, or 1 with a $metalnessmapHow metallic. "$metalness" "1" is a plain metal with no texture.
$roughnessfactor1Scales roughness. $metalness 1 with $roughnessfactor 0.1 is polished chrome.

Shading is GGX with height-correlated Smith and Schlick Fresnel – the model Source 2 and every other current engine settled on. A lightmapped surface reflects its nearest visible env_cubemap, blurred by roughness; one with no probe reflects an even glow the brightness of its own lightmap.

$surfaceprop is the physical type the surface is made of — concrete, metal, wood, and so on. It has two consumers. At runtime a trace that stops against a face reports the face’s material, and the engine reads that material’s $surfaceprop to choose a footstep sound (footstep/<type>/n). At compile time Resonance reads it for how much sound the surface soaks up, per band, from a table of the published figures for each type: carpet eats the highs, concrete eats almost nothing, snow eats almost everything. Unknown types parse to other rather than failing, so a game’s own surface names still work without the engine shipping them.

$acoustics overrides that, for the texture that looks like concrete and is meant to be acoustic tile: either four absorption coefficients for 125, 500, 2000 and 8000 Hz — "0.02 0.14 0.60 0.65" — or the name of a surface type to borrow them from, "carpet", leaving the footsteps as they were.

.kmdl — models

The resource container, kind KMDL, whose REFS block names every material the model draws with. Its DATA block is a 64-byte header, then vertices, indices, meshes, bones and a string table.

One model holds several meshes, each with its own material, because a single object routinely uses more than one. Splitting by material at compile time means one draw call per mesh instead of sorting at runtime.

Vertices carry four bone influences whether or not the model is skinned. Eight wasted bytes per vertex on a static prop buys one vertex layout, one shader path and no branch in the hot loop.

Bones are listed parents-first, so a single forward pass can build world transforms with no recursion and no sorting. A bone’s rest transform is its bind pose – the pose the mesh was skinned in – which is what the renderer inverts to move the mesh with the bones.

Animations (version 2). After the string table: one 16-byte record per clip – name, frame count, frames per second, a loop flag – then every clip’s keys, frame after frame, one 28-byte BoneKey (translation, rotation quaternion, both relative to the parent) per bone per frame. Clips are resampled at a fixed rate by Forge, so playing one is two lookups and an interpolation per bone. A version 1 file – every static prop compiled before animations existed – still loads, as a model with none.

Triangles are wound counter-clockwise as seen from the front – the convention the GPU renderer culls by, and the opposite of .kmap faces, which are clockwise. Forge takes an OBJ’s winding as-is (the axis remap preserves handedness), so the OBJ must already be counter-clockwise: a face wound the other way compiles and collides fine but renders inside out.

texture.kcfg — texture sets

A single PNG is not a surface. A surface is a colour, the bumps in it, how rough it is, what it glows with and where it self-shadows — five images that belong together and are useless apart. So a texture can also be a folder, under content/textures/:

content/textures/Walltextures/variant1/
    basecolor.png
    normal.png
    roughness.png
    emissive.png
    ao.png
    texture.kcfg      (optional)

It names itself from where it sits: Walltextures/variant1 compiles to Walltextures_variant1, and each map takes a suffix — _normal, _rough, _emissive, _ao. The path is already unique and already describes the thing, so restating it in a file would only be a second answer that can disagree with the first. Alchemy writes the matching .kmat wiring up whichever maps are there, once, and never overwrites one afterwards.

Filenames are matched generously (albedo, diffuse, col; nrm, bump; rough; glow, selfillum; occlusion) because those names come out of whatever tool baked them, and rejecting albedo.png because the engine wanted basecolor.png would be a rule that exists only to be tripped over. A folder with no base colour is not a set: the other maps modulate a colour, and with nothing to modulate they would compile into a material that draws the missing-texture checkerboard.

texture.kcfg is how you overrule any of that. Every key is optional — the point of the folder convention is that the common case needs no file at all:

texture
{
    "name"        "brick_red"     // instead of the folder-derived name
    "normal"      "weird_bumps.png"  // an image discovery would not find
    "shader"      "lit"
    "surfaceprop" "brick"
    "clamp"       "0"
    "point"       "0"
    "translucent" "0"
}

A key naming a file that is not there costs that one map, not the texture; a config that does not parse costs the config, not the folder. Losing a whole surface to a stray brace would be a poor trade.

To make one without editing anything by hand:

kerosene-tools alchemy new-texture Walls/brick     --basecolor brick.png --normal brick_n.png --surfaceprop brick

The images are copied in under their canonical stems, so the artist’s originals stay where they are and the content tree stays self-contained.

The loose art/*.png tree is unchanged and still compiles one image to one texture — right for a tool texture or a skybox, which have nothing to be a set with.

The developer texture set

Not a format, but content worth knowing the provenance of: alchemy dev-textures writes content/art/dev/ and content/art/tools/ along with their materials. The PNGs and .kmat files it writes are committed; only the compiled .ktex is not, because that is a build artefact.

dev/ are 256x256 checkerboards. At the default texture scale of 0.25 world units per texel that covers 64 ku, so the 4x4 grid on them reads as 16 ku cells – the size most brushwork is done at – with a fainter 4 ku subdivision over it. The checker is what makes a stretched texture obvious: a square that is not square is visible from across a room, where a stretched grid line is not. Only dev/measure carries numbers, because a world-aligned projection mirrors on opposite walls and text in a tiling texture therefore reads backwards on half of them.

tools/ are 128x128, flat, hatched, and labelled with their own name. A colour alone does not tell clip from playerclip at a glance.

They are generated because they are defined by numbers – this grid is 16 units, that colour means “blocks players” – and a definition that lives only in a PNG is one nobody can read or review. They are committed anyway, because a fresh clone with no textures in it is a fresh clone that looks broken: having a generator is a reason to keep one, not a reason to leave its output out of the repository. The generator reports what it changed and what was already correct, so re-running it on an unchanged tree is a no-op and a diff against it is meaningful.

Chisel runs the generator, and the batch compile after it, before it finishes loading. So a clone with the .ktex files missing – which is every clone, since they are build artefacts – still opens an editor with textures in it.

A test checks the set against the compiler’s tool-material table in both directions: offering a tool material the compiler treats as world geometry would silently wall off a doorway.

.kproj — the project file

Where a project says, rather than implies, where its content is.

project
{
    "name"     "My Mod"
    "content"  "content"
    "startmap" "mm_intro"
}

content is relative to the file, so a project can be cloned or moved anywhere and still be right. Every key is optional – an empty project { } block still marks a directory as a project, with content defaulting to a content/ directory beside it and then to the directory the file is in, which is the shape of a shipped game where the project file sits in the folder it describes. startmap is what kerosene loads when nothing else says which; a project that is a library of maps simply omits it rather than having one invented.

It exists because everything else about locating content is inference. The tools climb the directory tree looking for something shaped like a content root, which works, and is why a fresh clone needs no setup, and is still a guess – one nobody could overrule short of passing --content to every tool every time. A project file is the answer that wins.

Within any one place searched, a project file beats an inferred root even one found closer down: a stated answer that loses to a guess is not an answer. Between places, nearness still decides. The full order is in tools.md.

The content tree is created, not required

The directories under content/ — maps, materials, art, textures, models, sound, scripts — are made on first run by whatever starts first, the engine or the toolset, and any that are missing are filled in. A tree with one of them absent does not announce itself; it looks like whichever tool went looking for it being broken, which is the same class of problem the content-root search was written to end.

kerosene-tools init [dir] does it deliberately, writing a .kproj and the tree beside it. It is safe to run on an existing project: the project file is left exactly as it is, and only missing directories are created.

A project that wants a different layout says so, with repeated dir keys, and is believed — the same bargain as content:

project
{
    "content" "content"
    "dir"     "maps"
    "dir"     "materials"
    "dir"     "levels"
}

A project with no dir key gets the standard set, which is every project written so far. A dir naming a path outside the tree is ignored with a warning: the list is text somebody typed, and it does not get to create directories beside the project.

.kdef — entity class definitions

What an editor needs to know about the game’s entities: for each class, the keys it reads, the inputs it answers to and the outputs it fires. The engine never reads this — an entity there is a bag of whatever keys the map carries, and that is deliberate. The file exists so that a person placing a door is shown that speed and lip are things, instead of an empty panel.

It is the FGD relationship, and it is what keeps Chisel a separate program from the game: the editor reads the game’s file rather than linking its code.

base
{
    "name" "Entity"
    key   { "name" "targetname" "label" "Name" "type" "target_source"
            "help" "What other entities call this one." }
    input { "name" "Kill" "help" "Remove this entity from the map." }
}

class
{
    "name" "door"
    "kind" "brush"
    "base" "Entity"
    "help" "A brush that slides open and shut."
    key {
        "name" "spawnflags" "label" "Flags" "type" "flags" "default" "0"
        choice { "value" "1" "label" "Starts open" }
    }
    key    { "name" "speed" "type" "float" "default" "100" "help" "Units per second." }
    input  { "name" "SetSpeed" "parameter" "units per second" }
    output { "name" "OnFullyOpen" }
}

kind is point, brush or any, and decides which menu a class appears in. A class may name several base blocks; their keys, inputs and outputs come first, and a key the class redefines wins. Key types are string, int, float, bool, vec3, angles, color, target_source, target_destination, material, model, choices and flags — the set is closed, so an unrecognised type is an error at load rather than a text box at edit time.

A class may also declare helpers: what the editor draws for it beyond its marker – the FGD’s studio(), lightcone() and sphere(). They change nothing in the game.

class
{
    "name" "light_spot"
    "base" "Entity" "base" "Point" "base" "Angles"
    helper { "type" "lightcone" }
    helper { "type" "lightradius" }
}
class
{
    "name" "ambient_generic"
    helper { "type" "sphere" "radius" "radius" "color" "120 230 220" }
}
typedrawsparameters
modelthe entity’s model, posed by its angleskey (default model)
lightradiustwo spheres: where the light lights a surface fully, and where it has all but faded – from _light and the _*_attn keys, by the falloff Radiance uses
lightconea cone along the angles, from _cone and _inner_cone (half-angles), honouring the pitch override
spherea sphereradius, color
frustumwhat a camera seesfov (90), length (256)
directionan arrow along the angleslength (48)
linea line to every entity the key nameskey (default target)
recta flat rectangle facing along the angles: a panel, a decalwidth, height

A numeric parameter is a number or the name of a key: "radius" "radius" reads the entity’s own radius, "radius" "256" is always 256. Helpers are inherited from bases like keys, and a class’s own helper of a type replaces an inherited one. Two need no declaring: a class with a model-typed model key is drawn as its model, and every target_destination key draws a line to what it names. An unknown helper type is an error at load, naming the class.

Files are merged in sorted path order and a later definition of a class replaces an earlier one, so a mod can drop its own file beside the game’s.

A default is what the game assumes when a key is absent. Chisel shows it greyed rather than writing it into the map, so a .kmap only carries the keys someone chose — which is what makes a diff between two saves readable.

content/kerosene.kdef describes the sample game, and a test in kerosene-game checks it against the class registry in both directions: an input the game handles and the file does not offer is a build failure, and so is an input the file offers that nothing handles.

.kscr — level scripts

A map’s script, loaded automatically when the level starts if it is named after the map. The language is Rhai; the API, the hooks and the limits are in scripting.md.

fn on_map_start() {
    for door in find_by_class("door") {
        print(`${door} at ${door.origin}`);
    }
}

Nothing about the format is special — it is source text the engine hands to a VM. It is listed here because it is content the engine loads by name and packs into a .vault with everything else.

.ksnd — sound scripts

What a sound name means: which file, how loud, how far it carries. The format and the model behind it are in audio.md.

.vault — content archives

[ header 40 bytes ][ directory, sorted by path ][ data blob ]

Each directory record is path_len:u16, path, crc32:u32, offset:u64, size:u64. Paths are stored already normalised, so a lookup is a binary search with no per-query allocation.

Reads are checked against the stored CRC. A corrupt archive — a truncated download, a bad copy — is far better caught at the read than as a garbled texture three subsystems later.

Output is byte-for-byte reproducible: insertion order does not leak into the file, which matters for patching and for diffing releases.

Virtual paths

Asset paths arrive in every spelling — Materials\Dev\Grid, materials/dev/grid, ./materials//dev/grid — and all resolve to one key: lowercase, forward slashes, no redundant parts.

Paths that climb above the search root are refused, not clamped. Map and material files are untrusted content that ships between users, and ../../../../etc/passwd in a $basetexture must not resolve to anything.

Search paths are consulted in the order they were added, first hit wins, and loose files shadow packed ones — so during development you drop a file next to a shipped archive and it takes effect immediately, with no repack.