Spade is the SPASTAZ level editor. Part 1 covers the editor itself; Part 2 is the reference for writing mods. Download Spade.
Part 1 — Using the Editor
The window
| Region | Contents |
|---|---|
| Menu bar | File actions, Render, Help, Projects, Console, active project, snap toggles, grid readout. Wider than the window at this UI scale — hover it and scroll the wheel to reach the right-hand end. An arrow appears on the right edge while there is more to see. |
| Left column | Tools, quick light placement, mod tool buttons. Scrolls with the wheel. |
| Viewports | 3D perspective plus Top (X/Z), Front (X/Y) and Side (Y/Z). The 2D views show live cursor coordinates in the bottom-left corner. |
| Right panel | Seven tabs. Scrolls with the wheel; the scene tree is never truncated. |
| Status bar | Last message. Full history lives in the Console. |
The editor 3D view is deliberately plain: flat key-light shading on a dark background, with no sky, shadows or ambient occlusion. It is a working view. Press Render to see the scene through the full lighting pipeline.
Tools
Select (Q)
Click objects in any viewport. In 2D views, the smallest brush under the cursor wins, so a small object inside a large one stays reachable.
| Key | Action |
|---|---|
G / R / T | Grab, rotate, scale |
X / Y / Z | Lock the transform to one axis |
Enter or click | Confirm — Esc or right-click cancels |
Ctrl+C / V / D | Copy, paste, duplicate |
Ctrl+L / Ctrl+I | Flip horizontally / vertically |
Delete | Delete the selection |
H / Ctrl+H | Hide selection / unhide everything |
C | Centre every viewport on the selection |
Block (B)
Drag a box in a 2D view, press Enter to create the solid. The shape row in the menu bar picks cube, cylinder, sphere, arc or plane.
Clip (C)
Drag a cut line in a 2D view. The plane is that line extruded along the view's depth axis. Tick marks show which side survives. Several cuts can be schemed up and applied together, and holding Ctrl while dragging curves the cut.
| Input | Action |
|---|---|
Drag | Draw a straight cut |
Ctrl + drag | Draw a curved cut |
Wheel (curve pending) | Adjust the bulge |
Shift + wheel | Adjust how many segments approximate the arc (2–16) |
Tab | Cycle kept side: front, back, both |
Space | Queue this cut and start another (up to 8) |
Backspace | Remove the last queued cut |
Enter | Apply the whole queue |
Esc | Clear everything |
What the kept side means
- Front / Back — Each plane trims the solid. Chaining several gives their intersection — that is how you get chamfers, bevels and faceted shapes from one brush.
- Both — Each plane also emits its offcut as a separate brush. The original is decomposed into convex pieces that tile it exactly.
Cutting an arched doorway
Select the wall, switch to Clip, set the kept side to Both with Tab, then Ctrl+drag an arc across it and tune the bulge with the wheel. Press Enter: the wall splits into the arch piece plus the surrounding pieces. Delete the piece filling the opening.
A curved cut is approximated by its segments, so an 8-segment arc becomes 8 planes. The queue holds up to 32 planes in total, which is where the 8-stroke and 16-segment limits come from.
Clipping cannot create the bent faces that free vertex editing can, so it is the safer way to shape geometry.
Vertex / Morph (V)
Click a solid to activate it; Ctrl+click adds more solids and edits them together.
| Key | Action |
|---|---|
1 / 2 / 3 | Vertex, Edge or Face sub-mode |
Ctrl+click | Add or remove a handle from the selection |
Click same spot again | Cycle through handles stacked behind each other |
Alt+click (2D) | Grab every handle under the cursor at once |
Drag empty space | Box select; Enter or release commits |
Ctrl+A / Ctrl+Shift+A | Select / clear all handles |
Ctrl+B | Snap handles to grid |
Ctrl+M | Weld coincident vertices |
Delete | Collapse the selection to one point |
Ctrl+F | Split a quad face along a diagonal (pick two opposite corners) |
Ctrl+E | Edge mode: insert a vertex at the edge midpoint |
Ctrl+Shift+E | Face mode: extrude along the normal |
Ctrl+U | Face mode: subdivide into quads |
Shift+drag | Face mode: slide along the face normal |
Backspace | Reset handles to clean box corners |
Dropping a vertex onto another prompts to weld them — that is how a cube becomes a wedge or a pyramid. In a 2D view two corners often sit on the same pixel; a click takes the nearest, and clicking again steps to the one behind it.
Entity (E)
Place lights, props, spawns and physics objects. The kind row is in the menu bar. The left column also has one-click Point / Spot / Sun buttons that drop a light at the 3D view's focus, aimed where the camera is looking.
Texture (T)
Click a face in the 3D view to paint it. That is the whole workflow.
| Input | Action |
|---|---|
Click | Paint the face with the active texture |
Shift+click | Paint every face of that brush |
Alt+click | Sample the face — takes its texture and its alignment |
Ctrl+click | Paint, and also apply the sampled alignment |
Texturing
Every face carries its own texture, scale, offset, rotation, projection mode, flips and tint.
Projection modes
| Mode | Behaviour | Use for |
|---|---|---|
| World | UVs from world position. The texture is pinned to the world grid, so adjacent brushes line up automatically and sliding a brush slides it under the texture. | Walls, floors, anything tiling across several brushes |
| Local | UVs from brush-local position. The texture is locked to the brush and travels with it — Hammer's texture lock. | Props and detail pieces you will move around |
| Fit | UVs normalised to the face's own bounds: one tile covers the face exactly, whatever its shape. Scale becomes a tile count rather than world units. | Signs, posters, decals |
Controls (Obj tab, with a brush selected)
- Scale U/V — world units per repeat, or tile count in Fit mode
- Rot / R90 — degrees about the face normal; R90 steps in quarter turns
- Off U/V — shift the texture across the surface
- Flip U / Flip V — mirror either axis
- Face R/G/B — per-face colour multiplier
- Fit / World / Reset — one-click alignment
- Copy / Paste Placement — transfer alignment between faces without changing which texture is on them
With faces selected in morph face-mode every control applies to just those faces; otherwise it applies to the whole brush.
Panels
| Tab | Contents |
|---|---|
| Info | Counts and undo depth |
| Tree | Every brush and entity. Scrolls — nothing is truncated. |
| Obj | Properties of the selection. Every number has both a slider and a typeable box. |
| Mdl | Model assets with a live turntable preview |
| Tex | Scrollable thumbnail grid with hover preview and dimensions |
| Set | Lighting quality, snapping, editor settings |
| Mods | Loaded mods, per-tool status, and the last run's log |
Every numeric field accepts typed input. Type a value and click away or press Enter; out-of-range entries are clamped rather than rejected.
Camera
Hold right mouse in the 3D view:
| Key | Action |
|---|---|
Mouse | Look |
W A S D | Move |
Q / E | Straight down / up on the world axis |
Shift | 3× speed |
Wheel | Trim fly speed |
The view cannot roll — up is pinned to world up every frame — and pitch stops just short of vertical. In 2D views the wheel zooms and middle-drag pans.
Snapping
Four independent toggles in the menu bar: Mov, Rot, Scl, Vtx. Step sizes are in the Set tab; rotation defaults to 15°. The [ and ] keys halve and double the grid.
Projects
Projects are the main way in. The manager opens before the editor; if none exists one called Untitled is created.
projects/<name>/
maps/ .mbsptz files; Save and Open default here
exports/
assets/
models/
textures/
Opening a project loads its most recently saved map automatically. Save and Open list only that project's maps, newest first. A filename containing a slash or a drive letter is treated as an absolute path, which is how you export elsewhere.
Autosave writes _autosave.mbsptz every two minutes when something has changed. If it is newer than every real save when you open the project, you are offered recovery. It is hidden from the Open dialog.
Console
Toggle from the menu bar. Captures both editor messages and raylib's own TraceLog output, so shader compile errors and driver warnings land here alongside your own actions. Filter by Editor / Info / Warn / Error, scroll with the wheel, Follow sticks to the newest line. Everything is also printed to stdout, so running from a terminal behaves normally.
If the render window looks wrong, check here first.
Mod tools
Mods add buttons under MODS in the left column. There are two kinds:
- Command tools — Declared with tool= in the manifest. They fire once per click and return you to whatever you were doing. Each run is a single undo step.
- Mode tools — Declared with mode=. Clicking one makes it the SELECTED tool, exactly like Select or Clip: it stays highlighted and its script runs every frame until you pick something else. These can read the keyboard and mouse, so they behave like built-in tools.
A mode tool asks for its own undo steps with mod.pushundo, because pushing one every frame would fill the history in seconds. It can also stand itself down with mod.exit.
Diagnostics
Alt+P runs Check For Problems: bent (non-planar) faces, degenerate faces, coincident vertices, orphaned vertices and empty brushes. Auto-Fix All welds duplicates and recomputes winding.
Bent faces come from free vertex editing. Fix them by splitting the face along a diagonal (Ctrl+F), or use the Clip tool instead.
File layout
<executable>/
mods/ global mods, one subfolder each
projects/ all projects
resources/
shaders/ lighting.vs/.fs, depth.vs/.fs,
fullscreen.vs, ssao.fs, blur.fs
icons/ light.png, light_spot.png, light_env.png,
physics.png, player_start.png, entity.png
startup/ splash.png, splash.wav (both optional)
assets/ fallback models/ and textures/
Shaders are #version 330. If they fail to compile the editor falls back to CPU shading and says so in the Set tab — check the Console for the compiler output.
Part 2 — Modding Reference
Where mods live
<folder containing spade_editor.exe>/
mods/
my_mod/
mod.txt <- required
something.asm
another.asm
Mods are global, not per project: one mods/ folder next to the executable, one subfolder per mod. Loose .asm files sitting directly in mods/ are ignored — the scanner only looks at subfolders, and only ones containing a mod.txt.
Mods are scanned at startup. Mods tab → Reload Mods rescans without restarting.
The manifest
name = Example Pack
author = your name
# tool = fires once per click
tool = Pillar Row | pillars.asm | Place a row of 6 pillars
tool = Scatter | scatter.asm | Scatter cubes around the selection
# mode = becomes the SELECTED tool and runs every frame
mode = Paint Cubes | paint.asm | Click in a 2D view to drop cubes
One key = value per line. # and ; start comments. CRLF and LF both work. Limits: 16 mods, 12 tools each, 2048 instructions and 64 variables per script.
tool versus mode
| tool = | mode = | |
|---|---|---|
| Button behaves as | Push button | Toggle, stays lit while selected |
| Script runs | Once per click | Every frame while selected |
| Undo | Pushed automatically | Script calls mod.pushundo when it edits |
| Typical use | A command: scatter, taper, generate | A tool: paint, drag, place with the mouse |
Scripts assemble at load time, so a syntax error appears in the Mods tab as [!!] with a line number rather than when you click the button.
Language basics
A stack machine over 32-bit floats. One instruction per line, case-insensitive. Comments start with ; or #. Labels end with :.
PUSH 3 ; stack: 3
PUSH 4 ; stack: 3 4
ADD ; stack: 7
STORE total ; stack: empty, total = 7
There is no integer type; bitwise instructions truncate to int, operate, then convert back.
Operand order: the value pushed first is the left operand. PUSH 10 / PUSH 3 / SUB gives 7. The same holds for LT, DIV, POW and the rest.
Instruction set
Stack
| Op | Before → After | Notes |
|---|---|---|
PUSH n | → n | Literal |
POP / DROP | a → | Two spellings, one instruction |
DUP | a → a a | |
SWAP | a b → b a | |
OVER | a b → a b a | |
NIP | a b → b | |
TUCK | a b → b a b | |
ROT | a b c → b c a | |
RROT | a b c → c a b | Reverse rotate |
DEPTH | → n | Current stack size |
CLEAR | … → | Empties the stack |
Variables
| Op | Effect |
|---|---|
LOAD name | Push the variable |
STORE name | Pop into the variable |
INC name | name += 1, touches no stack |
DEC name | name -= 1 |
ADDV name | Pop a value and add it to the variable |
64 named slots, all starting at 0. INC is the idiomatic loop counter — shorter and faster than LOAD / PUSH 1 / ADD / STORE.
Binary arithmetic
| Op | Result |
|---|---|
ADD SUB MUL DIV | Basic arithmetic |
MOD | Float remainder, keeping the sign of the left operand |
IDIV | Integer division, truncated toward zero |
POW | a raised to b |
MIN MAX | Smaller / larger |
ATAN2 | atan2(a, b) |
HYPOT | sqrt(a² + b²) |
STEP | 1 if b >= a, else 0 (a is the edge) |
WRAP | a wrapped into [0, b). Always positive: -7 WRAP 3 is 2 |
SNAPTO | a rounded to the nearest multiple of b |
COPYSIGN | Magnitude of a, sign of b |
DIV, MOD and IDIV by zero abort with a line number rather than producing infinity. WRAP and SNAPTO by zero return a unchanged.
Unary arithmetic
| Op | Result |
|---|---|
NEG ABS SIGN | Sign manipulation; SIGN gives -1, 0 or 1 |
FLOOR CEIL ROUND TRUNC | Rounding |
FRACT | Fractional part, x - floor(x) |
RECIP | 1/x, or 0 when x is about zero |
SQR SQRT | Square and square root; SQRT of a negative gives 0 |
EXP LOG LOG2 LOG10 | Logs return 0 for non-positive input |
SIN COS TAN | Radians |
ASIN ACOS | Input clamped to -1..1 first |
ATAN | Arc tangent |
SINH COSH TANH | Hyperbolic |
DEG RAD | Convert between radians and degrees |
RAND | Pushes 0..1 |
RAND is seeded identically every run, so a mod produces the same result twice. That is deliberate: undo and re-run gives you the same thing.
Ternary
| Op | Args | Result |
|---|---|---|
CLAMP | v, lo, hi | v limited to the range; lo and hi are swapped if reversed |
LERP | a, b, t | a + (b - a) * t |
SMOOTHSTEP | edge0, edge1, x | Smooth 0..1 ramp |
BETWEEN | v, lo, hi | 1 if lo <= v <= hi |
Comparison, logic and bitwise
| Group | Ops | Notes |
|---|---|---|
| Comparison | LT GT LE GE EQ NE | Push 1 for true, 0 for false. EQ and NE use a 1e-6 tolerance, because exact float equality is a trap. |
| Logic | NOT AND OR XOR ISZERO ISNAN | Logical, treating any non-zero as true |
| Bitwise | BAND BOR BXOR BNOT SHL SHR | Operands truncate to int and convert back; shifts clamp to 0..31 |
Control flow
| Op | Effect |
|---|---|
JMP label | Jump |
JZ label | Pop; jump if zero |
JNZ label | Pop; jump if non-zero |
JEQ JNE JLT JGT JLE JGE | Pop two; compare left against right; jump if true |
GOSUB label | Call a subroutine, 32 levels deep |
RET | Return from GOSUB |
NOP | Nothing |
HALT | Stop. End of file also stops. |
The compare-and-jump family reads naturally as a loop test:
LOAD i
PUSH 10
JGE done ; if i >= 10, jump
Output
| Op | Effect |
|---|---|
PRINT | Pop and write to the run log |
CALL log | Log it but leave it on the stack |
Output appears in Mods tab → Last Run and in the Console.
Host functions
CALL name. Arguments push left to right and pop automatically; every call pushes exactly one result, so POP it if unused. Out-of-range indices return -1 or 0 and do nothing. There are 225 of them.
Selection (11)
| Call | Args | Description |
|---|---|---|
sel.brush | 0 | selected brush index, or -1 |
sel.entity | 0 | selected entity index, or -1 |
sel.setbrush | 1 | (index) select a brush |
sel.setentity | 1 | (index) select an entity |
sel.clear | 0 | clear the selection |
sel.tool | 0 | active tool 0..5 |
sel.settool | 1 | (tool) 0 sel 1 ent 2 blk 3 tex 4 vtx 5 clip |
sel.texture | 0 | active texture index |
sel.settexture | 1 | (index) set the active texture |
sel.model | 0 | active model index |
sel.setmodel | 1 | (index) set the active model |
Map (6)
| Call | Args | Description |
|---|---|---|
map.brushcount | 0 | number of brushes |
map.entcount | 0 | number of entities |
map.grid | 0 | grid step |
map.setgrid | 1 | (step) set the grid |
map.clearbrushes | 0 | delete every brush |
map.clearents | 0 | delete every entity |
Camera (21)
| Call | Args | Description |
|---|---|---|
cam.posx | 0 | 3D camera X |
cam.posy | 0 | 3D camera Y |
cam.posz | 0 | 3D camera Z |
cam.setpos | 3 | (x,y,z) move the camera, keeping its aim |
cam.targetx | 0 | look-at point X |
cam.targety | 0 | look-at point Y |
cam.targetz | 0 | look-at point Z |
cam.settarget | 3 | (x,y,z) aim at a point, keeping position |
cam.fwdx | 0 | forward vector X |
cam.fwdy | 0 | forward vector Y |
cam.fwdz | 0 | forward vector Z |
cam.yaw | 0 | yaw in degrees |
cam.pitch | 0 | pitch in degrees |
cam.setangles | 2 | (yaw,pitch) in degrees |
cam.lookat | 3 | (x,y,z) point the camera at a spot |
cam.fov | 0 | field of view |
cam.setfov | 1 | (degrees) set field of view |
cam.zoom | 0 | 2D viewport zoom |
cam.setzoom | 1 | (zoom) set 2D viewport zoom |
cam.orbit | 4 | (cx,cy,cz,radius) orbit a point at the current yaw |
cam.dolly | 1 | (distance) move along the view direction |
Snapping (10)
| Call | Args | Description |
|---|---|---|
snap.move | 0 | 1 if movement snapping is on |
snap.rot | 0 | 1 if rotation snapping is on |
snap.scale | 0 | 1 if scale snapping is on |
snap.vertex | 0 | 1 if vertex snapping is on |
snap.setmove | 1 | (0/1) |
snap.setrot | 1 | (0/1) |
snap.setscale | 1 | (0/1) |
snap.setvertex | 1 | (0/1) |
snap.rotstep | 0 | rotation step in degrees |
snap.setrotstep | 1 | (degrees) |
Settings (14)
| Call | Args | Description |
|---|---|---|
set.ambient | 0 | ambient light level |
set.setambient | 1 | (0..1) |
set.flyspeed | 0 | camera fly speed |
set.setflyspeed | 1 | (speed) |
set.gamma | 0 | render gamma |
set.setgamma | 1 | (gamma) |
set.specular | 0 | specular strength |
set.setspecular | 1 | (0..1) |
set.lighting | 0 | 1 if lighting is enabled |
set.setlighting | 1 | (0/1) |
set.gizmos | 0 | 1 if gizmo icons are shown |
set.setgizmos | 1 | (0/1) |
set.quality | 0 | render quality 0..3 |
set.setquality | 1 | (0 off 1 low 2 med 3 high) |
Brush (56)
| Call | Args | Description |
|---|---|---|
brush.vertcount | 1 | (brush) vertex count |
brush.facecount | 1 | (brush) face count |
brush.getx | 2 | (brush,vert) local X |
brush.gety | 2 | (brush,vert) local Y |
brush.getz | 2 | (brush,vert) local Z |
brush.setv | 5 | (brush,vert,x,y,z) |
brush.posx | 1 | (brush) origin X |
brush.posy | 1 | (brush) origin Y |
brush.posz | 1 | (brush) origin Z |
brush.setpos | 4 | (brush,x,y,z) |
brush.sizex | 1 | (brush) nominal size X |
brush.sizey | 1 | (brush) nominal size Y |
brush.sizez | 1 | (brush) nominal size Z |
brush.setsize | 4 | (brush,x,y,z) scales the mesh too |
brush.rotx | 1 | (brush) pitch |
brush.roty | 1 | (brush) yaw |
brush.rotz | 1 | (brush) roll |
brush.setrot | 4 | (brush,pitch,yaw,roll) |
brush.add | 7 | (shape,x,y,z,sx,sy,sz) -> index |
brush.delete | 1 | (brush) remove it |
brush.clone | 1 | (brush) -> new index |
brush.extrude | 3 | (brush,face,distance) |
brush.splitedge | 3 | (brush,va,vb) -> new vertex |
brush.weld | 2 | (brush,epsilon) -> removed |
brush.flip | 2 | (brush,axis 0/1/2) |
brush.subdivide | 2 | (brush,face) -> quads |
brush.fixwind | 1 | (brush) recompute winding |
brush.settex | 2 | (brush,textureIndex) |
brush.gettex | 1 | (brush) texture index |
brush.shape | 1 | (brush) shape it was built from |
brush.rebuild | 2 | (brush,shape) regenerate as a primitive |
brush.hide | 2 | (brush,0/1) |
brush.ishidden | 1 | (brush) 1 if hidden |
brush.physics | 1 | (brush) 1 if a physics object |
brush.setphysics | 2 | (brush,0/1) |
brush.mass | 1 | (brush) mass |
brush.setmass | 2 | (brush,mass) |
brush.friction | 1 | (brush) friction |
brush.setfriction | 2 | (brush,friction) |
brush.bounce | 1 | (brush) restitution |
brush.setbounce | 2 | (brush,bounce) |
brush.colr | 1 | (brush) colour red |
brush.colg | 1 | (brush) colour green |
brush.colb | 1 | (brush) colour blue |
brush.setcol | 4 | (brush,r,g,b) |
brush.minx | 1 | (brush) world bounds min X |
brush.miny | 1 | (brush) world bounds min Y |
brush.minz | 1 | (brush) world bounds min Z |
brush.maxx | 1 | (brush) world bounds max X |
brush.maxy | 1 | (brush) world bounds max Y |
brush.maxz | 1 | (brush) world bounds max Z |
brush.centrex | 1 | (brush) bounds centre X |
brush.centrey | 1 | (brush) bounds centre Y |
brush.centrez | 1 | (brush) bounds centre Z |
brush.volume | 1 | (brush) signed mesh volume |
brush.clip | 6 | (brush,nx,ny,nz,dist,keep 0=front 1=back 2=both) |
Face (30)
| Call | Args | Description |
|---|---|---|
face.vertcount | 2 | (brush,face) corner count |
face.vert | 3 | (brush,face,k) -> vertex index |
face.nx | 2 | (brush,face) normal X |
face.ny | 2 | (brush,face) normal Y |
face.nz | 2 | (brush,face) normal Z |
face.cx | 2 | (brush,face) centre X, local |
face.cy | 2 | (brush,face) centre Y, local |
face.cz | 2 | (brush,face) centre Z, local |
face.area | 2 | (brush,face) surface area |
face.tex | 2 | (brush,face) texture index, -1 = inherit |
face.settex | 3 | (brush,face,texture) |
face.scaleu | 2 | (brush,face) U scale |
face.scalev | 2 | (brush,face) V scale |
face.setscale | 4 | (brush,face,u,v) |
face.offu | 2 | (brush,face) U offset |
face.offv | 2 | (brush,face) V offset |
face.setoff | 4 | (brush,face,u,v) |
face.rot | 2 | (brush,face) texture rotation |
face.setrot | 3 | (brush,face,degrees) |
face.proj | 2 | (brush,face) 0 world 1 local 2 fit |
face.setproj | 3 | (brush,face,mode) |
face.flipu | 2 | (brush,face) 1 if U flipped |
face.flipv | 2 | (brush,face) 1 if V flipped |
face.setflip | 4 | (brush,face,flipU,flipV) |
face.tintr | 2 | (brush,face) tint red |
face.tintg | 2 | (brush,face) tint green |
face.tintb | 2 | (brush,face) tint blue |
face.settint | 5 | (brush,face,r,g,b) |
face.split | 4 | (brush,face,va,vb) diagonal split |
face.planarerr | 2 | (brush,face) how bent the face is |
Entity (37)
| Call | Args | Description |
|---|---|---|
ent.add | 4 | (kind,x,y,z) -> index |
ent.delete | 1 | (entity) remove it |
ent.posx | 1 | (entity) X |
ent.posy | 1 | (entity) Y |
ent.posz | 1 | (entity) Z |
ent.setpos | 4 | (entity,x,y,z) |
ent.rotx | 1 | (entity) pitch |
ent.roty | 1 | (entity) yaw |
ent.rotz | 1 | (entity) roll |
ent.setrot | 4 | (entity,pitch,yaw,roll) |
ent.aim | 4 | (entity,dx,dy,dz) point it along a vector |
ent.scale | 1 | (entity) scale |
ent.setscale | 2 | (entity,scale) |
ent.kind | 1 | (entity) kind 0..5 |
ent.setkind | 2 | (entity,kind) |
ent.setlight | 5 | (entity,r,g,b,brightness) |
ent.lightr | 1 | (entity) light red |
ent.lightg | 1 | (entity) light green |
ent.lightb | 1 | (entity) light blue |
ent.bright | 1 | (entity) brightness |
ent.range | 1 | (entity) light range |
ent.setrange | 2 | (entity,range) |
ent.inner | 1 | (entity) inner cone |
ent.outer | 1 | (entity) outer cone |
ent.setcone | 3 | (entity,inner,outer) |
ent.casts | 1 | (entity) 1 if it casts light |
ent.setcasts | 2 | (entity,0/1) |
ent.mass | 1 | (entity) mass |
ent.setmass | 2 | (entity,mass) |
ent.physics | 1 | (entity) 1 if a physics object |
ent.setphysics | 2 | (entity,0/1) |
ent.hide | 2 | (entity,0/1) |
ent.ishidden | 1 | (entity) 1 if hidden |
ent.model | 1 | (entity) model index |
ent.setmodel | 2 | (entity,model) |
ent.tex | 1 | (entity) texture index |
ent.settex | 2 | (entity,texture) |
Assets (3)
| Call | Args | Description |
|---|---|---|
asset.texcount | 0 | loaded texture count |
asset.modelcount | 0 | loaded model count |
asset.iconcount | 0 | loaded icon count |
System (7)
| Call | Args | Description |
|---|---|---|
sys.time | 0 | seconds since startup |
sys.fps | 0 | current frames per second |
sys.frametime | 0 | seconds for the last frame |
sys.screenw | 0 | window width |
sys.screenh | 0 | window height |
sys.mousex | 0 | mouse X |
sys.mousey | 0 | mouse Y |
Keyboard (8)
| Call | Args | Description |
|---|---|---|
key.down | 1 | (code) 1 while the key is held |
key.up | 1 | (code) 1 while the key is not held |
key.pressed | 1 | (code) 1 on the frame it goes down |
key.released | 1 | (code) 1 on the frame it comes up |
key.shift | 0 | 1 while either shift is held |
key.ctrl | 0 | 1 while either control is held |
key.alt | 0 | 1 while either alt is held |
key.any | 0 | keycode pressed this frame, or 0 |
Mouse (10)
| Call | Args | Description |
|---|---|---|
mouse.down | 1 | (button 0=L 1=R 2=M) 1 while held |
mouse.pressed | 1 | (button) 1 on the frame it goes down |
mouse.released | 1 | (button) 1 on the frame it comes up |
mouse.wheel | 0 | wheel movement this frame |
mouse.dx | 0 | mouse movement X this frame |
mouse.dy | 0 | mouse movement Y this frame |
mouse.vp | 0 | viewport under the cursor: 0 3D, 1 top, 2 front, 3 side, -1 none |
mouse.worldx | 0 | cursor X in world space (2D views) |
mouse.worldy | 0 | cursor Y in world space (2D views) |
mouse.worldz | 0 | cursor Z in world space (2D views) |
Mod Tool State (7)
| Call | Args | Description |
|---|---|---|
mod.ismode | 0 | 1 if this tool was declared with mode= |
mod.active | 0 | 1 if this tool is the selected tool right now |
mod.frame | 0 | frames since this tool became active |
mod.exit | 0 | deselect this tool and return to Select |
mod.get | 1 | (slot 0..31) read state that survives between frames |
mod.set | 2 | (slot,value) write persistent state |
mod.pushundo | 0 | record an undo step before the next edit |
Utility (5)
| Call | Args | Description |
|---|---|---|
snap | 1 | (value) snapped to the grid |
log | 1 | (value) log it, unchanged |
dist3 | 6 | (x1,y1,z1,x2,y2,z2) distance |
dot3 | 6 | (x1,y1,z1,x2,y2,z2) dot product |
len3 | 3 | (x,y,z) vector length |
Safety
Mods edit your map, so the interpreter is deliberately paranoid:
- Step limit — 2,000,000 instructions. An infinite loop reports 'step limit reached' instead of hanging the editor.
- Stack limit — 128 entries; underflow and overflow both abort with a line number.
- Return stack — 32 levels; deeper GOSUB nesting aborts.
- Bounds checks — Every brush, vertex, face, and entity index is checked against the live map.
- Undo — Each run is wrapped in an undo push, so Ctrl+Z reverts the whole thing.
- Division by zero — Aborts rather than producing infinity.
A failed run leaves the map as it was at the point of failure; it does not roll back automatically, but Ctrl+Z will.
Worked example
Taper a brush: pull every vertex above the midline toward the centre line.
CALL sel.brush
STORE b
LOAD b
PUSH 0
JLT done ; nothing selected
LOAD b
CALL brush.vertcount
STORE n
PUSH 0.45
STORE k
PUSH 0
STORE i
loop:
LOAD i
LOAD n
JGE finish
LOAD b
LOAD i
CALL brush.gety
STORE vy
LOAD vy
PUSH 0
JLE next ; leave the bottom half alone
LOAD b
LOAD i
CALL brush.getx
LOAD k
MUL
STORE nx
LOAD b
LOAD i
CALL brush.getz
LOAD k
MUL
STORE nz
LOAD b
LOAD i
LOAD nx
LOAD vy
LOAD nz
CALL brush.setv
POP
next:
INC i
JMP loop
finish:
LOAD b
CALL brush.fixwind
POP
done:
HALT
Call brush.fixwind after moving vertices by hand. Pulling a vertex through a face inverts it, and an inverted face renders inside-out.
Camera example
Frame the selected brush: aim the camera at its centre from a distance based on its size.
CALL sel.brush
STORE b
LOAD b
PUSH 0
JLT done
; centre of the brush
LOAD b
CALL brush.centrex
STORE cx
LOAD b
CALL brush.centrey
STORE cy
LOAD b
CALL brush.centrez
STORE cz
; radius from the bounding box diagonal
LOAD b
CALL brush.maxx
LOAD b
CALL brush.minx
SUB
STORE w
LOAD w
PUSH 2.5
MUL
PUSH 4
MAX ; never closer than 4 units
STORE dist
LOAD cx
LOAD cy
LOAD cz
LOAD dist
CALL cam.orbit
POP
done:
HALT
Reading input
key.down, key.pressed and key.released take a raylib key code. Letters and digits are their ASCII values, so 'A' is 65 and '0' is 48. Common codes:
| Key | Code | Key | Code |
|---|---|---|---|
| Space | 32 | Escape | 256 |
| Enter | 257 | Tab | 258 |
| Backspace | 259 | Delete | 261 |
| Right arrow | 262 | Left arrow | 263 |
| Down arrow | 264 | Up arrow | 265 |
| A … Z | 65 … 90 | 0 … 9 | 48 … 57 |
| Left shift | 340 | Left control | 341 |
| Left alt | 342 | F1 | 290 |
key.shift, key.ctrl and key.alt cover the modifiers without needing codes. Mouse buttons are 0 left, 1 right, 2 middle.
mouse.vp reports which viewport the cursor is over (0 3D, 1 top, 2 front, 3 side, -1 none), and mouse.worldx/y/z give the cursor position in world space for the 2D views — which is what you need to place anything under the pointer.
Mode tool example
A tool that drops a grid-snapped cube wherever you click in a 2D view, and exits on Escape. Declare it with mode= so it stays selected.
; exit when Escape is pressed
PUSH 256
CALL key.pressed
JZ carry_on
CALL mod.exit
POP
HALT
carry_on:
; only act on a fresh left click inside a 2D viewport
PUSH 0
CALL mouse.pressed
JZ done
CALL mouse.vp
STORE vp
LOAD vp
PUSH 1
JLT done ; -1 or 0 means not a 2D view
; snap the cursor position to the grid
CALL mouse.worldx
CALL snap
STORE px
CALL mouse.worldy
CALL snap
STORE py
CALL mouse.worldz
CALL snap
STORE pz
; one undo step per placement, not per frame
CALL mod.pushundo
POP
PUSH 0 ; cube
LOAD px
LOAD py
LOAD pz
PUSH 2
PUSH 2
PUSH 2
CALL brush.add
POP
; count what we placed, in state that survives the frame
PUSH 0
CALL mod.get
PUSH 1
ADD
STORE total
PUSH 0
LOAD total
CALL mod.set
POP
LOAD total
CALL log
POP
done:
HALT
mod.get and mod.set give each tool 32 float slots that persist between frames. Ordinary variables are cleared every run, so anything a mode tool needs to remember goes in a slot.
Idioms
Counted loop — there is no FOR.
PUSH 0
STORE i
loop:
LOAD i
PUSH 10
JGE done
; body
INC i
JMP loop
done:
A subroutine. Put them after HALT so they only run when called.
PUSH 4
GOSUB cube_at_x
HALT
cube_at_x:
STORE px
PUSH 0
LOAD px
PUSH 0
PUSH 0
PUSH 2
PUSH 2
PUSH 2
CALL brush.add
POP
RET
Discard results. Every CALL pushes something; ignore it inside a loop and the stack overflows.
Error messages
| Message | Cause |
|---|---|
unknown instruction 'X' | Typo, or no such instruction |
unknown function 'X' | Typo in a CALL name; they are case-sensitive |
no such label 'X' | Jump target never defined |
stack underflow | Popped more than pushed — usually a missing operand |
stack overflow | Usually a CALL result never POPped inside a loop |
step limit reached | Counter never advances, or the exit test is inverted |
divide by zero | Guard the divisor first |
RET without GOSUB | Fell into a subroutine instead of jumping past it |
GOSUB nested too deep | Recursion deeper than 32 levels |
missing script X | mod.txt names a file not in the folder |