File formats
| Extension | What | Text or binary | Written by | Source analogue |
|---|---|---|---|---|
.kmap | Editable map source | text (KeyValues) | Chisel | .vmf |
.kbsp | Compiled map | binary, lump directory | Cleave / Umbra / Resonance / Radiance | .bsp |
.kprt | Portal graph | text | Cleave | .prt |
.kleak | Leak trace | text | Cleave | .lin |
.kwalk | NPC walkmap | binary | Cleave | (Source has no equivalent) |
.ktex | Texture | binary, resource container | Alchemy | .vtf |
.kmat | Material source | text (KeyValues) | Alchemy, by hand | .vmt |
.kmat_c | Compiled material | binary, resource container | Alchemy | .vmat_c |
texture.kcfg | Texture set definition | text (KeyValues) | Alchemy, by hand | (no equivalent) |
.kmdl | Model | binary, resource container | Forge | .mdl |
.kdef | Entity class definitions | text (KeyValues) | the game, by hand | .fgd |
.kscr | Level script, UI script | text (Rhai) | by hand | .nut (VScript) |
.kui | UI layout | text (XML) | by hand | Panorama .xml |
.kcss | UI stylesheet | text (CSS subset) | by hand | Panorama .css |
.ksnd | Sound script | text (KeyValues) | by hand | game_sounds.txt |
.kaud | Compiled sound | binary | Timbre | .wav (ADPCM) |
.wav .flac .mp3 | Sound sources | binary | any audio tool | .wav |
.vault | Content archive | binary | Vault | .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.
| Lump | Holds |
|---|---|
| entities | KeyValues text, one block per entity |
| planes | Pairs: index n^1 is always the inverse of n |
| vertices, edges, surfedges | Shared vertex positions and the ring indirection |
| faces | Renderable polygons, with lightmap extents |
| nodes, leaves | The BSP tree |
| leaffaces, leafbrushes | What each leaf holds |
| models | Model 0 is the world; 1..n are brush entities |
| brushes, brushsides | Convex collision volumes |
| texinfo, texdata, texdata_strings | Materials and their projections |
| visibility | The run-length-encoded PVS, and the PAS beside it |
| lighting | Baked lightmap samples |
| acoustics | One record per room: how long sound lingers there, per band |
| acoustic_leafs | One u16 per leaf: which room it is in |
| sections | The streamed sections: a name and bounds for each; section 0 is the world |
| face_sections, brush_sections | One 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.
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | KRES |
| 4 | 4 | Container version, 1 |
| 8 | 4 | Kind: four bytes naming the payload, KMAT, KTEX, KMDL |
| 12 | 4 | Kind version |
| 16 | 8 | Source hash: 64-bit FNV-1a of the source it was compiled from |
| 24 | 4 | Block count |
| 28 | 4 | Reserved, 0 |
| 32 | 12 × count | Block 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.
| Parameter | What it does |
|---|---|
$basetexture | The colour. Sampled through an sRGB view. |
$bumpmap | Tangent-space normals. The basis comes from the face’s texture projection, so it agrees with the UVs by construction. |
$roughness | Red channel, 0 mirror to 1 matte. Drives the specular term; with no map (and no $roughnessfactor) a dielectric is fully rough and gets none. |
$selfillummask | What the surface emits regardless of light reaching it. Added in linear HDR before tone-mapping, so it rolls off like a lit highlight. |
$ao | Baked occlusion, multiplied into the lighting. Darkens what the surface shadows itself, which the lightmap’s luxels are far too coarse to see. |
$metalnessmap | Red 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.
| Parameter | Default | What it does |
|---|---|---|
$metalness | 0, or 1 with a $metalnessmap | How metallic. "$metalness" "1" is a plain metal with no texture. |
$roughnessfactor | 1 | Scales 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" }
}
| type | draws | parameters |
|---|---|---|
model | the entity’s model, posed by its angles | key (default model) |
lightradius | two 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 | |
lightcone | a cone along the angles, from _cone and _inner_cone (half-angles), honouring the pitch override | |
sphere | a sphere | radius, color |
frustum | what a camera sees | fov (90), length (256) |
direction | an arrow along the angles | length (48) |
line | a line to every entity the key names | key (default target) |
rect | a flat rectangle facing along the angles: a panel, a decal | width, 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.