Sandboxed, Safe, Simple

Lua generators run inside a strict sandbox. Only the scene construction API and safe Lua standard libraries are available. There is no file I/O, no network access, no process spawning.

This makes generators easy to distribute, safe to run from untrusted sources, and impossible to accidentally corrupt the host system.

Blocked modules: io, os, debug, require, dofile, loadfile.
Allowed: math, string, table, ipairs, pairs, type, tostring, tonumber, print, pcall, error.

Zero-Config Registration

The LuaGeneratorRegistry scans generators/lua/ recursively at startup. Every .lua file found is executed to extract its M.id string. No configuration file or registration step is needed.

Subdirectory structure is for human organisation only — the registry does not enforce categories. The ID string in M.id is the only thing that matters for lookup.

Override rule: If a Lua generator has the same ID as a C++ generator, the Lua version wins. Use this to override any built-in generator without modifying C++ source.

Generator Module Format

lua — minimal generator
local M = {}

-- Required: unique generator ID
M.id       = "lua.zone.my_zone"

-- Optional but recommended
M.version  = "0.1.0"
M.category = "zone"

function M.generate(ctx, scene)
    scene:addGround("grass")
    scene:setMetadata({
        generator = {
            id       = M.id,
            version  = M.version,
            category = M.category,
            language = "lua"
        },
        generation = {
            variationInput = ctx.variation
        }
    })
end

return M
FieldRequiredDescription
M.idrequiredUnique generator identifier. Must match the convention lua.<cat>.<name>.<variant>.
M.versionoptionalSemantic version string. Used in metadata output. Defaults to "0.0.0" if omitted.
M.categoryoptionalCategory tag: zone, object, building, room. For documentation and metadata.
M.generaterequiredFunction accepting (ctx, scene). Called once per chunk generation request.

The ctx Parameter

The first parameter to M.generate is a context table containing information about the chunk being generated.

FieldTypeDescription
ctx.variationintegerPer-chunk seed derived from the world seed and chunk coordinates. Use for deterministic pseudo-random placement.
ctx.chunk_xintegerX coordinate of this chunk in the world grid (0-based).
ctx.chunk_yintegerY coordinate of this chunk in the world grid (0-based).
ctx.chunk_size_mnumberSide length of the chunk in metres (default 64). Use this instead of hardcoding 64.
ctx.stylestringWorld style name (e.g. "central_europe_small_city"). Can drive material selection.
ctx.zonestringZone type for this chunk (e.g. "city", "forest", "ocean").
ctx.regionstringRegion type for this chunk (e.g. "park", "small_house_block", "crossroad").
ctx.parameterstable or nilOptional parameter table for object/building generators. Caller passes this to override defaults.
Seeded random: Use ctx.variation as your seed rather than math.random() to keep generation deterministic across platforms and runs.

The scene Parameter

scene:addGround (material: string)

Places a ground plane covering the entire chunk at y=0. Every chunk should call this exactly once.

scene:addGround("grass")
scene:addGround("asphalt")
scene:addGround("sand")
scene:addBox (id: string, params: table)

Adds an axis-aligned rectangular box. The most versatile primitive — used for walls, floors, furniture, signs, and almost everything else.

Param keyTypeRequiredDescription
position{x, y, z}yesCentre of the box in world space. y=0 is the ground plane.
size{w, h, d}yesWidth (X), height (Y), depth (Z) of the box in metres.
materialstringyesMaterial name. Must exist in the MaterialRegistry.
rynumberoptionalRotation around the Y axis in degrees. Default 0.
scene:addBox("wall_front", {
    position = {0, 1.5, 5},
    size     = {10, 3, 0.3},
    material = "plaster_white",
    ry       = 0
})

scene:addBox("bench_seat", {
    position = {20, 0.46, 15},
    size     = {1.55, 0.04, 0.35},
    material = "wood_bench"
})
scene:addCylinder (id: string, params: table)

Adds a vertical cylinder. Used for tree trunks, lamp poles, fountain basins, columns, pillars, and round objects.

Param keyTypeRequiredDescription
position{x, y, z}yesBase centre of the cylinder (bottom face centre) in world space.
radiusnumberyesRadius in metres.
heightnumberyesHeight in metres.
materialstringyesMaterial name. Must exist in the MaterialRegistry.
scene:addCylinder("lamp_pole", {
    position = {15, 0, 18},
    radius   = 0.055,
    height   = 4.5,
    material = "metal_lamp"
})

scene:addCylinder("tree_trunk", {
    position = {10, 0, 10},
    radius   = 0.18,
    height   = 4.0,
    material = "wood_bark_dark"
})
scene:addPlane (id: string, params: table)

Adds a flat horizontal plane at a specific position. Used for roads, pavements, plazas, water surfaces, and decorative overlays on the ground.

Param keyTypeRequiredDescription
position{x, y, z}yesTop-left corner of the plane. y should be slightly above 0 (e.g. 0.01) to avoid z-fighting with the ground.
size{w, d}yesWidth (X) and depth (Z) of the plane in metres.
materialstringyesMaterial name.
-- Central plaza (8×8 m at chunk centre)
scene:addPlane("plaza", {
    position = {28, 0.01, 28},
    size     = {8, 8},
    material = "stone_pavement"
})

-- North-south path (2 m wide, full chunk length)
scene:addPlane("path_ns", {
    position = {31, 0.01, 0},
    size     = {2, 64},
    material = "stone_path"
})
scene:addInstance (id: string, params: table)

Embeds another generator's output at a specific position within this chunk. Used to compose complex scenes from reusable object generators.

Param keyTypeRequiredDescription
generatorstringyesGenerator ID to instantiate (e.g. "lua.object.bench.simple").
position{x, y, z}yesPosition offset for the instance's origin.
rynumberoptionalY rotation in degrees.
parameterstableoptionalPassed as ctx.parameters to the child generator.
scene:addInstance("bench_n", {
    generator  = "lua.object.bench.simple",
    position   = {32, 0, 26},
    ry         = 0,
    parameters = { width = 1.8, material = "wood_bench" }
})

scene:addInstance("house_1", {
    generator  = "lua.building.simple_house.standard",
    position   = {5, 0, 5},
    ry         = 90
})
scene:setMetadata (meta: table)

Sets the metadata block of the MC3 output file. Required for MC3 validation to pass — a chunk without valid metadata will be rejected.

FieldRequiredDescription
meta.generator.idrequiredGenerator ID string. Use M.id.
meta.generator.versionrequiredVersion string. Use M.version.
meta.generator.languagerequiredAlways "lua" for Lua generators.
meta.generation.variationInputoptionalThe variation seed used. Aids reproducibility.
meta.chunk.x, meta.chunk.yoptionalChunk coordinates from ctx. Useful for debugging.
scene:setMetadata({
    generator  = {
        id       = M.id,
        version  = M.version,
        category = M.category,
        language = "lua"
    },
    chunk      = {
        x      = ctx.chunk_x,
        y      = ctx.chunk_y,
        size_m = ctx.chunk_size_m
    },
    generation = {
        variationInput = ctx.variation,
        zone           = ctx.zone,
        region         = ctx.region
    }
})

Full Park Zone Generator

This is the actual generators/lua/zone/park.lua shipped with MeshWorld. It demonstrates all scene API methods, seeded randomness, and helper functions.

lua — generators/lua/zone/park.lua
-- SPDX-License-Identifier: MIT
local M = {}
M.id       = "lua.zone.park"
M.version  = "0.1.0"
M.category = "zone"

-- Seeded pseudo-random (no global state)
local function rng(seed, i)
    local v = (seed * 1664525 + i * 22695477 + 1013904223) % (2^32)
    return v / (2^32)
end

local function place_bench(scene, id, x, z, ry)
    local w, sh = 1.6, 0.44
    scene:addBox(id.."_legl", {position={x-w/2+0.1,sh/2,z}, size={0.08,sh,0.5}, material="metal_lamp", ry=ry})
    scene:addBox(id.."_legr", {position={x+w/2-0.1,sh/2,z}, size={0.08,sh,0.5}, material="metal_lamp", ry=ry})
    scene:addBox(id.."_seat", {position={x,sh+0.02,z},         size={w-0.05,0.04,0.35}, material="wood_bench", ry=ry})
    scene:addBox(id.."_back", {position={x,sh+0.30,z-0.19},  size={w-0.05,0.35,0.04}, material="wood_bench", ry=ry})
end

local function place_lamp(scene, id, x, z, h)
    h = h or 4.5
    scene:addCylinder(id.."_base", {position={x,0,z}, radius=0.12, height=0.06, material="metal_lamp"})
    scene:addCylinder(id.."_pole", {position={x,0,z}, radius=0.055, height=h, material="metal_lamp"})
    scene:addBox(id.."_head",     {position={x,h,z}, size={0.35,0.18,0.35}, material="metal_lamp"})
end

local SPECIES = {
    {tr=0.18, th=4.0, cr=3.2, tm="wood_bark_dark",  cm="foliage_oak"      },
    {tr=0.15, th=3.5, cr=2.8, tm="wood_bark_light", cm="foliage_linden"   },
    {tr=0.10, th=5.0, cr=2.0, tm="wood_birch",      cm="foliage_birch"    },
    {tr=0.20, th=4.5, cr=3.5, tm="wood_bark_dark",  cm="foliage_chestnut" },
}
local function place_tree(scene, id, x, z, seed_i, scale)
    scale = scale or 1.0
    local sp = SPECIES[math.fmod(seed_i, 4) + 1]
    local cy = sp.th * scale * 0.7
    scene:addCylinder(id.."_trunk",  {position={x,0,z},  radius=sp.tr*scale, height=sp.th*scale, material=sp.tm})
    scene:addBox     (id.."_canopy", {position={x,cy,z}, size={sp.cr*scale*2,sp.cr*scale*1.4,sp.cr*scale*2}, material=sp.cm})
    scene:addBox     (id.."_top",    {position={x,cy+sp.cr*scale*0.6,z}, size={sp.cr*scale*1.3,sp.cr*scale*0.7,sp.cr*scale*1.3}, material=sp.cm})
end

function M.generate(ctx, scene)
    local S    = ctx.chunk_size_m
    local half = S / 2
    local var  = ctx.variation or 0

    scene:addGround("grass")

    scene:addPlane("plaza",   {position={half-4,0.01,half-4}, size={8,8},  material="stone_pavement"})
    scene:addPlane("path_ns", {position={half-1,0.01,0},      size={2,S},  material="stone_path"})
    scene:addPlane("path_ew", {position={0,0.01,half-1},      size={S,2},  material="stone_path"})

    scene:addCylinder("fountain_basin",  {position={half,0,half},   radius=1.8, height=0.4, material="stone_wall"})
    scene:addCylinder("fountain_column", {position={half,0.4,half}, radius=0.2, height=1.2, material="stone_wall"})
    scene:addCylinder("fountain_cap",    {position={half,1.5,half}, radius=0.5, height=0.2, material="stone_pavement"})

    place_bench(scene, "bench_n", half,        half-6, 0)
    place_bench(scene, "bench_s", half,        half+6, 0)
    place_bench(scene, "bench_w", half-6, half,      90)
    place_bench(scene, "bench_e", half+6, half,      90)

    local lamps = {{half,8},{half,S-8},{8,half},{S-8,half},{half-12,half},{half+12,half}}
    for i, lp in ipairs(lamps) do
        place_lamp(scene, "lamp_"..i, lp[1], lp[2])
    end

    local trees = {{10,10},{18,8},{8,20},{14,18},{54,10},{46,8},
                   {56,20},{50,18},{10,54},{18,56},{8,46},{14,50},
                   {54,54},{46,56},{56,46},{50,50}}
    for i, tp in ipairs(trees) do
        place_tree(scene, "tree_"..i, tp[1], tp[2], var+i, 0.8+rng(var,i)*0.5)
    end

    scene:setMetadata({
        generator  = {id=M.id, version=M.version, category=M.category, language="lua"},
        chunk      = {x=ctx.chunk_x, y=ctx.chunk_y, size_m=S},
        generation = {variationInput=var, zone=ctx.zone, region=ctx.region}
    })
end

return M