Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commitdd348ac6c3

README.md

procPack — a family of procedural ops as content

A .procPack is a FAMILY of procedural operations authored as content: a folder whose init.luau returns a pack table. When the asset registers, every def in its ops map joins the proc op registry under the stable string id the pack names it with, so a graph calls it by that id (g.op.<id>). Discover the available packs with asset.list("procPack"), and the ops any of them contributed with procgen.searchNodes("", { pack = "<name>" }).

This is the one path an op family reaches the registry through. Procgen's own families are packs — procPacks/ in this package holds one per family, abi.procPack beside it holds the ops every node is built from, and terrain's terrain.procPack sits in its own package the same way — so the packs shipped here are worked examples of exactly what you are writing, not a separate mechanism the built-ins are exempt from.

MyPack.procPack/
  init.luau     -- returns the pack table
  README.md
  .metadata

The pack table

local proc = require("@builtin::systems.procgen.proc")

local function transformed(ctx: any, geometry: any, params: { [string]: any }): any
    return proc.registry.get("geometry.transform").eval(ctx, { geometry = geometry }, params)
end

return {
    name = "myPack",                        -- the asset's own name when omitted
    description = "Moves and turns whole geometries.",
    ops = {
        ["myPack.lift"] = {
            description = "Lift a Geometry by a height.",
            inputs = { geometry = { type = "Geometry" } },
            params = { height = { type = "Float", default = 1.0 } },
            outputs = { geometry = { type = "Geometry", default = true } },
            eval = function(ctx, inputs, params)
                return transformed(ctx, inputs.geometry, { translate = { x = 0, y = params.height, z = 0 } })
            end,
        },
        ["myPack.spin"] = {
            description = "Turn a Geometry about the up axis.",
            inputs = { geometry = { type = "Geometry" } },
            params = { degrees = { type = "Float", default = 45 } },
            outputs = { geometry = { type = "Geometry", default = true } },
            eval = function(ctx, inputs, params)
                return transformed(ctx, inputs.geometry, { rotate = { x = 0, y = params.degrees, z = 0 } })
            end,
        },
    },
}

Each def is the same table every op in the registry is described by — typed input sockets, params, typed output sockets, and an eval(ctx, inputs, params) returning the outputs. ctx carries seed / nodeId / compositePath / hash / checkpoint. Validation is the same for every pack, so a malformed socket or an undeclared param type is refused here exactly as it is for the ones shipped in this package.

Beside name, description and ops, the pack table takes:

  • context: the context every op of the family works in ("geometry", "field", "sim", ...), for each def that states none of its own. A name that is no registered context is refused.
  • aliases: former op ids mapped onto current ones ({ ["myPack.raise"] = "myPack.lift" }). A graph naming a former id cooks the current op, while listings and schemas carry the current id. An alias onto an id that is no registered op is refused, and so is an alias whose former id is itself a registered op.

A graph calls a pack's op as g.op.<id>, with one table naming its inputs and params:

return proc.define(function(g)
    local box = g.mesh.box({})
    local lifted = g.op.myPack.lift({ geometry = box, height = 2 })
    return { mesh = g.op.myPack.spin({ geometry = lifted }) }
end)

procPack or procNode

Both contribute nodes that a graph calls. They differ in how a node is written and addressed:

.procNode.procPack
contributesone node, written as a graph (proc.define(function(g) ... end))a family of ops, each a def table
addressed bythe asset's identity (g.points.disc, a path, a guid or a short name), recorded in the graph by guidthe string id the pack names it with
a graph writesg.<family>.<name>({ ... })g.op.myPack.lift({ ... })

Reach for a .procPack when the ops belong together and you want to read the family's name at every call site; reach for a .procNode when one operation stands on its own.

Finding a pack's ops

Search embeds a pack's README.md and init.luau (the indexing block of type.yaml), so the README is where a search by what an op does reaches it: list every op of the pack as a line of its own, the op's id and what it does, and keep the line true when the op changes.

- `myPack.lift` — Lift a Geometry by a height.
- `myPack.spin` — Turn a Geometry about the up axis.

procgen.searchNodes asks for .procNode and .procPack assets together. A hit that is a pack is listed as the ops of that pack that answer the query's words (op.myPack.lift, with matched.pack naming the pack), or as the pack itself when none of them does.

Registration lifecycle

init.luau is compiled from its own source on every registration rather than required, so an edit lands immediately. Another module is reached by its absolute name (@builtin::systems.procgen.proc), and a file of the pack's own by a relative require (require(".helpers")). Every op's version is derived from the pack's source, unless its def declares a version of its own, so editing the file re-keys the node caches of every graph that reaches any op in the family — those graphs recook.

Editing the pack to REMOVE an op unregisters it. A graph built before the edit that still names the op errors loudly at eval, naming the op that is not registered, and a graph source that calls it fails to build, naming the ops the pack still holds. Deleting the .procPack folder unregisters the whole family the same way.

In the Inspector

Operations: how many ops the pack registers, and each op's id, kind and what it does.